Merge remote-tracking branch 'tuskyapp/develop'
This commit is contained in:
commit
1228f645a6
|
@ -7,7 +7,7 @@
|
||||||
|
|
||||||
Yuito is fork of [Tusky](https://github.com/tuskyapp/Tusky).
|
Yuito is fork of [Tusky](https://github.com/tuskyapp/Tusky).
|
||||||
|
|
||||||
Tusky is a beautiful Android client for [Mastodon](https://github.com/tootsuite/mastodon). Mastodon is an ActivityPub federated social network. That means no single entity controls the whole network, rather, like e-mail, volunteers and organisations operate their own independent servers, users from which can all interact with each other seamlessly.
|
Tusky is a beautiful Android client for [Mastodon](https://github.com/mastodon/mastodon). Mastodon is an ActivityPub federated social network. That means no single entity controls the whole network, rather, like e-mail, volunteers and organisations operate their own independent servers, users from which can all interact with each other seamlessly.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
|
@ -15,14 +15,14 @@ Tusky is a beautiful Android client for [Mastodon](https://github.com/tootsuite/
|
||||||
- Most Mastodon APIs implemented
|
- Most Mastodon APIs implemented
|
||||||
- Multi-Account support
|
- Multi-Account support
|
||||||
- Dark, light and black themes with the possibility to auto-switch based on the time of day
|
- Dark, light and black themes with the possibility to auto-switch based on the time of day
|
||||||
- Drafts - compose toots and save them for later
|
- Drafts - compose posts and save them for later
|
||||||
- Choose between different emoji styles
|
- Choose between different emoji styles
|
||||||
- Optimized for all screen sizes
|
- Optimized for all screen sizes
|
||||||
- Completely open-source - no non-free dependencies like Google services
|
- Completely open-source - no non-free dependencies like Google services
|
||||||
|
|
||||||
### Support
|
### Support
|
||||||
|
|
||||||
If you have any bug reports, feature requests or questions please open an issue or send us a toot at [@ars42525@odakyu.app](https://odakyu.app/@ars42525)!
|
If you have any bug reports, feature requests or questions please open an issue or send us a message at [@ars42525@odakyu.app](https://odakyu.app/@ars42525)!
|
||||||
|
|
||||||
For translating Tusky into your language, visit https://weblate.tusky.app/
|
For translating Tusky into your language, visit https://weblate.tusky.app/
|
||||||
|
|
||||||
|
|
|
@ -15,11 +15,11 @@ def getGitSha = {
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
compileSdkVersion 30
|
compileSdkVersion 31
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId 'net.accelf.yuito'
|
applicationId 'net.accelf.yuito'
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 30
|
targetSdkVersion 31
|
||||||
versionCode 43
|
versionCode 43
|
||||||
versionName '4.1.3'
|
versionName '4.1.3'
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
@ -96,12 +96,12 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.coroutinesVersion = "1.6.0"
|
ext.coroutinesVersion = "1.6.0"
|
||||||
ext.lifecycleVersion = "2.3.1"
|
ext.lifecycleVersion = "2.4.1"
|
||||||
ext.roomVersion = '2.3.0'
|
ext.roomVersion = '2.4.2'
|
||||||
ext.retrofitVersion = '2.9.0'
|
ext.retrofitVersion = '2.9.0'
|
||||||
ext.okhttpVersion = '4.9.3'
|
ext.okhttpVersion = '4.9.3'
|
||||||
ext.glideVersion = '4.12.0'
|
ext.glideVersion = '4.13.1'
|
||||||
ext.daggerVersion = '2.40.5'
|
ext.daggerVersion = '2.41'
|
||||||
ext.materialdrawerVersion = '8.4.5'
|
ext.materialdrawerVersion = '8.4.5'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
|
@ -117,33 +117,35 @@ dependencies {
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$coroutinesVersion"
|
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$coroutinesVersion"
|
||||||
|
|
||||||
implementation "androidx.core:core-ktx:1.5.0"
|
implementation "androidx.core:core-ktx:1.7.0"
|
||||||
implementation "androidx.appcompat:appcompat:1.3.0"
|
implementation "androidx.appcompat:appcompat:1.4.1"
|
||||||
implementation "androidx.fragment:fragment-ktx:1.3.4"
|
implementation "androidx.fragment:fragment-ktx:1.4.1"
|
||||||
implementation "androidx.browser:browser:1.3.0"
|
implementation "androidx.browser:browser:1.4.0"
|
||||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||||
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
||||||
implementation "androidx.cardview:cardview:1.0.0"
|
implementation "androidx.cardview:cardview:1.0.0"
|
||||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||||
implementation "androidx.sharetarget:sharetarget:1.1.0"
|
implementation "androidx.sharetarget:sharetarget:1.2.0-rc01"
|
||||||
implementation "androidx.emoji:emoji:1.1.0"
|
implementation "androidx.emoji:emoji:1.1.0"
|
||||||
implementation "androidx.emoji:emoji-appcompat:1.1.0"
|
implementation "androidx.emoji:emoji-appcompat:1.1.0"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||||
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
|
||||||
implementation "androidx.constraintlayout:constraintlayout:2.1.2"
|
implementation "androidx.constraintlayout:constraintlayout:2.1.3"
|
||||||
implementation "androidx.paging:paging-runtime-ktx:3.0.0"
|
implementation "androidx.paging:paging-runtime-ktx:3.1.1"
|
||||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||||
implementation "androidx.work:work-runtime:2.5.0"
|
implementation "androidx.work:work-runtime:2.7.1"
|
||||||
implementation "androidx.room:room-ktx:$roomVersion"
|
implementation "androidx.room:room-ktx:$roomVersion"
|
||||||
|
implementation "androidx.room:room-paging:$roomVersion"
|
||||||
implementation "androidx.room:room-rxjava3:$roomVersion"
|
implementation "androidx.room:room-rxjava3:$roomVersion"
|
||||||
kapt "androidx.room:room-compiler:$roomVersion"
|
kapt "androidx.room:room-compiler:$roomVersion"
|
||||||
|
implementation 'androidx.core:core-splashscreen:1.0.0-beta02'
|
||||||
|
|
||||||
implementation "com.google.android.material:material:1.4.0"
|
implementation "com.google.android.material:material:1.5.0"
|
||||||
|
|
||||||
implementation "com.google.code.gson:gson:2.8.9"
|
implementation "com.google.code.gson:gson:2.9.0"
|
||||||
|
|
||||||
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
||||||
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
||||||
|
@ -158,14 +160,14 @@ dependencies {
|
||||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
|
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
|
||||||
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
||||||
|
|
||||||
implementation "com.github.penfeizhou.android.animation:glide-plugin:2.17.0"
|
implementation "com.github.penfeizhou.android.animation:glide-plugin:2.20.0"
|
||||||
|
|
||||||
implementation "io.reactivex.rxjava3:rxjava:3.0.12"
|
implementation "io.reactivex.rxjava3:rxjava:3.1.3"
|
||||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||||
implementation "io.reactivex.rxjava3:rxkotlin:3.0.1"
|
implementation "io.reactivex.rxjava3:rxkotlin:3.0.1"
|
||||||
|
|
||||||
implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.0.0"
|
implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.1.1"
|
||||||
implementation "com.uber.autodispose2:autodispose:2.0.0"
|
implementation "com.uber.autodispose2:autodispose:2.1.1"
|
||||||
|
|
||||||
implementation "com.google.dagger:dagger:$daggerVersion"
|
implementation "com.google.dagger:dagger:$daggerVersion"
|
||||||
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
|
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
|
||||||
|
|
|
@ -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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<color name="notification_color">#19A341</color>
|
||||||
|
|
||||||
|
</resources>
|
|
@ -21,37 +21,23 @@
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/TuskyTheme"
|
android:theme="@style/TuskyTheme"
|
||||||
android:usesCleartextTraffic="false">
|
android:usesCleartextTraffic="false">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".SplashActivity"
|
android:name=".components.login.LoginActivity"
|
||||||
android:theme="@style/SplashTheme">
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
</activity>
|
||||||
|
<activity android:name=".components.login.LoginWebViewActivity" />
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize"
|
||||||
|
android:theme="@style/SplashTheme"
|
||||||
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="android.app.shortcuts"
|
|
||||||
android:resource="@xml/share_shortcuts" />
|
|
||||||
|
|
||||||
</activity>
|
|
||||||
<activity
|
|
||||||
android:name=".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=".MainActivity"
|
|
||||||
android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize">
|
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
|
@ -98,6 +84,9 @@
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.service.chooser.chooser_target_service"
|
android:name="android.service.chooser.chooser_target_service"
|
||||||
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
|
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.app.shortcuts"
|
||||||
|
android:resource="@xml/share_shortcuts" />
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
|
@ -107,7 +96,6 @@
|
||||||
<activity
|
<activity
|
||||||
android:name=".ViewThreadActivity"
|
android:name=".ViewThreadActivity"
|
||||||
android:configChanges="orientation|screenSize" />
|
android:configChanges="orientation|screenSize" />
|
||||||
<activity android:name=".ViewTagActivity" />
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ViewMediaActivity"
|
android:name=".ViewMediaActivity"
|
||||||
android:theme="@style/TuskyBaseTheme" />
|
android:theme="@style/TuskyBaseTheme" />
|
||||||
|
@ -125,7 +113,8 @@
|
||||||
android:theme="@style/Base.Theme.AppCompat" />
|
android:theme="@style/Base.Theme.AppCompat" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".components.search.SearchActivity"
|
android:name=".components.search.SearchActivity"
|
||||||
android:launchMode="singleTop">
|
android:launchMode="singleTop"
|
||||||
|
android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEARCH" />
|
<action android:name="android.intent.action.SEARCH" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
@ -135,18 +124,18 @@
|
||||||
android:resource="@xml/searchable" />
|
android:resource="@xml/searchable" />
|
||||||
</activity>
|
</activity>
|
||||||
<activity android:name=".ListsActivity" />
|
<activity android:name=".ListsActivity" />
|
||||||
<activity android:name=".ModalTimelineActivity" />
|
|
||||||
<activity android:name=".LicenseActivity" />
|
<activity android:name=".LicenseActivity" />
|
||||||
<activity android:name=".FiltersActivity" />
|
<activity android:name=".FiltersActivity" />
|
||||||
<activity
|
<activity
|
||||||
android:name=".components.report.ReportActivity"
|
android:name=".components.report.ReportActivity"
|
||||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||||
<activity android:name=".components.instancemute.InstanceListActivity" />
|
<activity android:name=".components.instancemute.InstanceListActivity" />
|
||||||
<activity android:name=".components.scheduled.ScheduledTootActivity" />
|
<activity android:name=".components.scheduled.ScheduledStatusActivity" />
|
||||||
<activity android:name=".components.announcements.AnnouncementsActivity" />
|
<activity android:name=".components.announcements.AnnouncementsActivity" />
|
||||||
<activity android:name=".components.drafts.DraftsActivity" />
|
<activity android:name=".components.drafts.DraftsActivity" />
|
||||||
|
|
||||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
<receiver android:name=".receiver.NotificationClearBroadcastReceiver"
|
||||||
|
android:exported="false" />
|
||||||
<receiver
|
<receiver
|
||||||
android:name=".receiver.SendStatusBroadcastReceiver"
|
android:name=".receiver.SendStatusBroadcastReceiver"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
|
@ -155,15 +144,17 @@
|
||||||
<service
|
<service
|
||||||
android:name=".service.TuskyTileService"
|
android:name=".service.TuskyTileService"
|
||||||
android:icon="@drawable/ic_tusky"
|
android:icon="@drawable/ic_tusky"
|
||||||
android:label="Compose Toot"
|
android:label="@string/tusky_compose_post_quicksetting_label"
|
||||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
|
||||||
|
android:exported="true"
|
||||||
tools:targetApi="24">
|
tools:targetApi="24">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service android:name=".service.SendTootService" />
|
<service android:name=".service.SendStatusService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
@ -177,10 +168,15 @@
|
||||||
|
|
||||||
<!-- disable automatic WorkManager initialization -->
|
<!-- disable automatic WorkManager initialization -->
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
android:name="androidx.startup.InitializationProvider"
|
||||||
android:authorities="${applicationId}.workmanager-init"
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
tools:node="remove" />
|
tools:node="merge">
|
||||||
|
<meta-data
|
||||||
|
android:name="androidx.work.WorkManagerInitializer"
|
||||||
|
android:value="androidx.startup"
|
||||||
|
tools:node="remove" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
<activity android:name="net.accelf.yuito.AccessTokenLoginActivity" />
|
<activity android:name="net.accelf.yuito.AccessTokenLoginActivity" />
|
||||||
</application>
|
</application>
|
||||||
|
|
|
@ -34,7 +34,7 @@ import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding
|
||||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
import com.keylesspalace.tusky.util.Either
|
import com.keylesspalace.tusky.util.Either
|
||||||
|
@ -49,7 +49,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private typealias AccountInfo = Pair<Account, Boolean>
|
private typealias AccountInfo = Pair<TimelineAccount, Boolean>
|
||||||
|
|
||||||
class AccountsInListFragment : DialogFragment(), Injectable {
|
class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
|
|
||||||
|
@ -168,21 +168,21 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
viewModel.deleteAccountFromList(listId, accountId)
|
viewModel.deleteAccountFromList(listId, accountId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onAddToList(account: Account) {
|
private fun onAddToList(account: TimelineAccount) {
|
||||||
viewModel.addAccountToList(listId, account)
|
viewModel.addAccountToList(listId, account)
|
||||||
}
|
}
|
||||||
|
|
||||||
private object AccountDiffer : DiffUtil.ItemCallback<Account>() {
|
private object AccountDiffer : DiffUtil.ItemCallback<TimelineAccount>() {
|
||||||
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean {
|
override fun areItemsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean {
|
||||||
return oldItem == newItem
|
return oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean {
|
override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean {
|
||||||
return oldItem.deepEquals(newItem)
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class Adapter : ListAdapter<Account, BindingHolder<ItemFollowRequestBinding>>(AccountDiffer) {
|
inner class Adapter : ListAdapter<TimelineAccount, BindingHolder<ItemFollowRequestBinding>>(AccountDiffer) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
|
||||||
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
@ -209,12 +209,11 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
||||||
|
|
||||||
private object SearchDiffer : DiffUtil.ItemCallback<AccountInfo>() {
|
private object SearchDiffer : DiffUtil.ItemCallback<AccountInfo>() {
|
||||||
override fun areItemsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
override fun areItemsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
||||||
return oldItem == newItem
|
return oldItem.first.id == newItem.first.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
||||||
return oldItem.second == newItem.second &&
|
return oldItem == newItem
|
||||||
oldItem.first.deepEquals(newItem.first)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,7 @@ import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
import com.keylesspalace.tusky.adapter.AccountSelectionAdapter;
|
import com.keylesspalace.tusky.adapter.AccountSelectionAdapter;
|
||||||
|
import com.keylesspalace.tusky.components.login.LoginActivity;
|
||||||
import com.keylesspalace.tusky.db.AccountEntity;
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
import com.keylesspalace.tusky.di.Injectable;
|
||||||
|
|
|
@ -1,358 +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.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.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -44,6 +44,7 @@ import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.emoji.text.EmojiCompat
|
import androidx.emoji.text.EmojiCompat
|
||||||
import androidx.emoji.text.EmojiCompat.InitCallback
|
import androidx.emoji.text.EmojiCompat.InitCallback
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
@ -74,9 +75,10 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canH
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsRepository
|
import com.keylesspalace.tusky.components.conversation.ConversationsRepository
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
||||||
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||||
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
||||||
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
|
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity
|
||||||
import com.keylesspalace.tusky.components.search.SearchActivity
|
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||||
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
import com.keylesspalace.tusky.components.timeline.TimelineFragment
|
||||||
import com.keylesspalace.tusky.databinding.ActivityMainBinding
|
import com.keylesspalace.tusky.databinding.ActivityMainBinding
|
||||||
|
@ -132,6 +134,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import net.accelf.yuito.CustomUncaughtExceptionHandler
|
||||||
import net.accelf.yuito.FooterDrawerItem
|
import net.accelf.yuito.FooterDrawerItem
|
||||||
import net.accelf.yuito.QuickTootViewModel
|
import net.accelf.yuito.QuickTootViewModel
|
||||||
import net.accelf.yuito.streaming.StreamingManager
|
import net.accelf.yuito.streaming.StreamingManager
|
||||||
|
@ -185,8 +188,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
Thread.setDefaultUncaughtExceptionHandler(CustomUncaughtExceptionHandler(applicationContext))
|
||||||
|
|
||||||
|
installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
// delete old notification channels
|
||||||
|
NotificationHelper.deleteLegacyNotificationChannels(this, accountManager)
|
||||||
|
|
||||||
val activeAccount = accountManager.activeAccount
|
val activeAccount = accountManager.activeAccount
|
||||||
?: return // will be redirected to LoginActivity by BaseActivity
|
?: return // will be redirected to LoginActivity by BaseActivity
|
||||||
|
|
||||||
|
@ -515,10 +524,10 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
nameRes = R.string.action_access_scheduled_toot
|
nameRes = R.string.action_access_scheduled_posts
|
||||||
iconRes = R.drawable.ic_access_time
|
iconRes = R.drawable.ic_access_time
|
||||||
onClick = {
|
onClick = {
|
||||||
startActivityWithSlideInAnimation(ScheduledTootActivity.newIntent(context))
|
startActivityWithSlideInAnimation(ScheduledStatusActivity.newIntent(context))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
primaryDrawerItem {
|
primaryDrawerItem {
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
/* Copyright 2018 Conny Duck
|
|
||||||
*
|
|
||||||
* This file is a part of Tusky.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
||||||
* Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
|
||||||
import net.accelf.yuito.CustomUncaughtExceptionHandler
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
class SplashActivity : AppCompatActivity(), Injectable {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var accountManager: AccountManager
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
val customUncaughtExceptionHandler = CustomUncaughtExceptionHandler(applicationContext)
|
|
||||||
Thread.setDefaultUncaughtExceptionHandler(customUncaughtExceptionHandler)
|
|
||||||
|
|
||||||
/** delete old notification channels */
|
|
||||||
NotificationHelper.deleteLegacyNotificationChannels(this, accountManager)
|
|
||||||
|
|
||||||
/** Determine whether the user is currently logged in, and if so go ahead and load the
|
|
||||||
* timeline. Otherwise, start the activity_login screen. */
|
|
||||||
|
|
||||||
val intent = if (accountManager.activeAccount != null) {
|
|
||||||
Intent(this, MainActivity::class.java)
|
|
||||||
} else {
|
|
||||||
LoginActivity.getIntent(this, false)
|
|
||||||
}
|
|
||||||
startActivity(intent)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -36,6 +36,7 @@ import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.core.app.ShareCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
|
@ -205,10 +206,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
|
|
||||||
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
val request = DownloadManager.Request(Uri.parse(url))
|
val request = DownloadManager.Request(Uri.parse(url))
|
||||||
request.setDestinationInExternalPublicDir(
|
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
|
||||||
Environment.DIRECTORY_PICTURES,
|
|
||||||
getString(R.string.app_name) + "/" + filename
|
|
||||||
)
|
|
||||||
downloadManager.enqueue(request)
|
downloadManager.enqueue(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -255,11 +253,11 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shareFile(file: File, mimeType: String?) {
|
private fun shareFile(file: File, mimeType: String?) {
|
||||||
val sendIntent = Intent()
|
ShareCompat.IntentBuilder(this)
|
||||||
sendIntent.action = Intent.ACTION_SEND
|
.setType(mimeType)
|
||||||
sendIntent.putExtra(Intent.EXTRA_STREAM, FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file))
|
.addStream(FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file))
|
||||||
sendIntent.type = mimeType
|
.setChooserTitle(R.string.send_media_to)
|
||||||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_media_to)))
|
.startChooser()
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isCreating: Boolean = false
|
private var isCreating: Boolean = false
|
||||||
|
|
|
@ -18,7 +18,7 @@ import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
import com.keylesspalace.tusky.util.removeDuplicates
|
import com.keylesspalace.tusky.util.removeDuplicates
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
||||||
protected val animateAvatar: Boolean,
|
protected val animateAvatar: Boolean,
|
||||||
protected val animateEmojis: Boolean
|
protected val animateEmojis: Boolean
|
||||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder?>() {
|
) : RecyclerView.Adapter<RecyclerView.ViewHolder?>() {
|
||||||
var accountList = mutableListOf<Account>()
|
var accountList = mutableListOf<TimelineAccount>()
|
||||||
private var bottomLoading: Boolean = false
|
private var bottomLoading: Boolean = false
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
override fun getItemCount(): Int {
|
||||||
|
@ -73,12 +73,12 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(newAccounts: List<Account>) {
|
fun update(newAccounts: List<TimelineAccount>) {
|
||||||
accountList = removeDuplicates(newAccounts)
|
accountList = removeDuplicates(newAccounts)
|
||||||
notifyDataSetChanged()
|
notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addItems(newAccounts: List<Account>) {
|
fun addItems(newAccounts: List<TimelineAccount>) {
|
||||||
val end = accountList.size
|
val end = accountList.size
|
||||||
val last = accountList[end - 1]
|
val last = accountList[end - 1]
|
||||||
if (newAccounts.none { it.id == last.id }) {
|
if (newAccounts.none { it.id == last.id }) {
|
||||||
|
@ -100,7 +100,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeItem(position: Int): Account? {
|
fun removeItem(position: Int): TimelineAccount? {
|
||||||
if (position < 0 || position >= accountList.size) {
|
if (position < 0 || position >= accountList.size) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
@ -109,7 +109,7 @@ abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructo
|
||||||
return account
|
return account
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addItem(account: Account, position: Int) {
|
fun addItem(account: TimelineAccount, position: Int) {
|
||||||
if (position < 0 || position > accountList.size) {
|
if (position < 0 || position > accountList.size) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import androidx.preference.PreferenceManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
|
@ -33,9 +33,9 @@ public class AccountViewHolder extends RecyclerView.ViewHolder {
|
||||||
showBotOverlay = sharedPrefs.getBoolean("showBotOverlay", true);
|
showBotOverlay = sharedPrefs.getBoolean("showBotOverlay", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setupWithAccount(Account account, boolean animateAvatar, boolean animateEmojis) {
|
public void setupWithAccount(TimelineAccount account, boolean animateAvatar, boolean animateEmojis) {
|
||||||
accountId = account.getId();
|
accountId = account.getId();
|
||||||
String format = username.getContext().getString(R.string.status_username_format);
|
String format = username.getContext().getString(R.string.post_username_format);
|
||||||
String formattedUsername = String.format(format, account.getUsername());
|
String formattedUsername = String.format(format, account.getUsername());
|
||||||
username.setText(formattedUsername);
|
username.setText(formattedUsername);
|
||||||
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
|
CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis);
|
||||||
|
|
|
@ -22,7 +22,7 @@ import android.widget.ImageView
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
@ -55,11 +55,11 @@ class BlocksAdapter(
|
||||||
private val unblock: ImageButton = itemView.findViewById(R.id.blocked_user_unblock)
|
private val unblock: ImageButton = itemView.findViewById(R.id.blocked_user_unblock)
|
||||||
private var id: String? = null
|
private var id: String? = null
|
||||||
|
|
||||||
fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) {
|
fun setupWithAccount(account: TimelineAccount, animateAvatar: Boolean, animateEmojis: Boolean) {
|
||||||
id = account.id
|
id = account.id
|
||||||
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
|
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
|
||||||
displayName.text = emojifiedName
|
displayName.text = emojifiedName
|
||||||
val format = username.context.getString(R.string.status_username_format)
|
val format = username.context.getString(R.string.post_username_format)
|
||||||
val formattedUsername = String.format(format, account.username)
|
val formattedUsername = String.format(format, account.username)
|
||||||
username.text = formattedUsername
|
username.text = formattedUsername
|
||||||
val avatarRadius = avatar.context.resources
|
val avatarRadius = avatar.context.resources
|
||||||
|
|
|
@ -22,7 +22,7 @@ import android.text.style.StyleSpan
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
@ -34,7 +34,7 @@ class FollowRequestViewHolder(
|
||||||
private val showHeader: Boolean
|
private val showHeader: Boolean
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) {
|
fun setupWithAccount(account: TimelineAccount, animateAvatar: Boolean, animateEmojis: Boolean) {
|
||||||
val wrappedName = account.name.unicodeWrap()
|
val wrappedName = account.name.unicodeWrap()
|
||||||
val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis)
|
val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis)
|
||||||
binding.displayNameTextView.text = emojifiedName
|
binding.displayNameTextView.text = emojifiedName
|
||||||
|
@ -45,7 +45,7 @@ class FollowRequestViewHolder(
|
||||||
}.emojify(account.emojis, itemView, animateEmojis)
|
}.emojify(account.emojis, itemView, animateEmojis)
|
||||||
}
|
}
|
||||||
binding.notificationTextView.visible(showHeader)
|
binding.notificationTextView.visible(showHeader)
|
||||||
val format = itemView.context.getString(R.string.status_username_format)
|
val format = itemView.context.getString(R.string.post_username_format)
|
||||||
val formattedUsername = String.format(format, account.username)
|
val formattedUsername = String.format(format, account.username)
|
||||||
binding.usernameTextView.text = formattedUsername
|
binding.usernameTextView.text = formattedUsername
|
||||||
val avatarRadius = binding.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
val avatarRadius = binding.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
||||||
|
|
|
@ -9,7 +9,7 @@ import android.widget.TextView
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
|
@ -69,7 +69,7 @@ class MutesAdapter(
|
||||||
private var notifications = false
|
private var notifications = false
|
||||||
|
|
||||||
fun setupWithAccount(
|
fun setupWithAccount(
|
||||||
account: Account,
|
account: TimelineAccount,
|
||||||
mutingNotifications: Boolean?,
|
mutingNotifications: Boolean?,
|
||||||
animateAvatar: Boolean,
|
animateAvatar: Boolean,
|
||||||
animateEmojis: Boolean
|
animateEmojis: Boolean
|
||||||
|
@ -77,7 +77,7 @@ class MutesAdapter(
|
||||||
id = account.id
|
id = account.id
|
||||||
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
|
val emojifiedName = account.name.emojify(account.emojis, displayName, animateEmojis)
|
||||||
displayName.text = emojifiedName
|
displayName.text = emojifiedName
|
||||||
val format = username.context.getString(R.string.status_username_format)
|
val format = username.context.getString(R.string.post_username_format)
|
||||||
val formattedUsername = String.format(format, account.username)
|
val formattedUsername = String.format(format, account.username)
|
||||||
username.text = formattedUsername
|
username.text = formattedUsername
|
||||||
val avatarRadius = avatar.context.resources
|
val avatarRadius = avatar.context.resources
|
||||||
|
|
|
@ -41,10 +41,10 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
|
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
|
||||||
import com.keylesspalace.tusky.entity.Emoji;
|
import com.keylesspalace.tusky.entity.Emoji;
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
|
@ -339,7 +339,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
this.statusDisplayOptions = statusDisplayOptions;
|
this.statusDisplayOptions = statusDisplayOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMessage(Account account) {
|
void setMessage(TimelineAccount account) {
|
||||||
Context context = message.getContext();
|
Context context = message.getContext();
|
||||||
|
|
||||||
String format = context.getString(R.string.notification_follow_format);
|
String format = context.getString(R.string.notification_follow_format);
|
||||||
|
@ -350,7 +350,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
);
|
);
|
||||||
message.setText(emojifiedMessage);
|
message.setText(emojifiedMessage);
|
||||||
|
|
||||||
String username = context.getString(R.string.status_username_format, account.getUsername());
|
String username = context.getString(R.string.post_username_format, account.getUsername());
|
||||||
usernameView.setText(username);
|
usernameView.setText(username);
|
||||||
|
|
||||||
CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify(
|
CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify(
|
||||||
|
@ -447,7 +447,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
|
|
||||||
private void setUsername(String name) {
|
private void setUsername(String name) {
|
||||||
Context context = username.getContext();
|
Context context = username.getContext();
|
||||||
String format = context.getString(R.string.status_username_format);
|
String format = context.getString(R.string.post_username_format);
|
||||||
String usernameText = String.format(format, name);
|
String usernameText = String.format(format, name);
|
||||||
username.setText(usernameText);
|
username.setText(usernameText);
|
||||||
}
|
}
|
||||||
|
@ -545,9 +545,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
||||||
contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE);
|
||||||
if (statusViewData.isExpanded()) {
|
if (statusViewData.isExpanded()) {
|
||||||
contentWarningButton.setText(R.string.status_content_warning_show_less);
|
contentWarningButton.setText(R.string.post_content_warning_show_less);
|
||||||
} else {
|
} else {
|
||||||
contentWarningButton.setText(R.string.status_content_warning_show_more);
|
contentWarningButton.setText(R.string.post_content_warning_show_more);
|
||||||
}
|
}
|
||||||
|
|
||||||
contentWarningButton.setOnClickListener(view -> {
|
contentWarningButton.setOnClickListener(view -> {
|
||||||
|
@ -649,10 +649,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
|
|
||||||
contentCollapseButton.setVisibility(View.VISIBLE);
|
contentCollapseButton.setVisibility(View.VISIBLE);
|
||||||
if (statusViewData.isCollapsed()) {
|
if (statusViewData.isCollapsed()) {
|
||||||
contentCollapseButton.setText(R.string.status_content_warning_show_more);
|
contentCollapseButton.setText(R.string.post_content_warning_show_more);
|
||||||
statusContent.setFilters(COLLAPSE_INPUT_FILTER);
|
statusContent.setFilters(COLLAPSE_INPUT_FILTER);
|
||||||
} else {
|
} else {
|
||||||
contentCollapseButton.setText(R.string.status_content_warning_show_less);
|
contentCollapseButton.setText(R.string.post_content_warning_show_less);
|
||||||
statusContent.setFilters(NO_INPUT_FILTER);
|
statusContent.setFilters(NO_INPUT_FILTER);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -200,7 +200,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
protected void setUsername(String name) {
|
protected void setUsername(String name) {
|
||||||
Context context = username.getContext();
|
Context context = username.getContext();
|
||||||
String usernameText = context.getString(R.string.status_username_format, name);
|
String usernameText = context.getString(R.string.post_username_format, name);
|
||||||
username.setText(usernameText);
|
username.setText(usernameText);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,9 +245,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
private void setContentWarningButtonText(boolean expanded) {
|
private void setContentWarningButtonText(boolean expanded) {
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
contentWarningButton.setText(R.string.status_content_warning_show_less);
|
contentWarningButton.setText(R.string.post_content_warning_show_less);
|
||||||
} else {
|
} else {
|
||||||
contentWarningButton.setText(R.string.status_content_warning_show_more);
|
contentWarningButton.setText(R.string.post_content_warning_show_more);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -588,9 +588,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
sensitiveMediaWarning.setText(R.string.status_sensitive_media_title);
|
sensitiveMediaWarning.setText(R.string.post_sensitive_media_title);
|
||||||
} else {
|
} else {
|
||||||
sensitiveMediaWarning.setText(R.string.status_media_hidden_title);
|
sensitiveMediaWarning.setText(R.string.post_media_hidden_title);
|
||||||
}
|
}
|
||||||
|
|
||||||
sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE);
|
sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE);
|
||||||
|
@ -635,7 +635,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
private void updateMediaLabel(int index, boolean sensitive, boolean showingContent) {
|
private void updateMediaLabel(int index, boolean sensitive, boolean showingContent) {
|
||||||
Context context = itemView.getContext();
|
Context context = itemView.getContext();
|
||||||
CharSequence label = (sensitive && !showingContent) ?
|
CharSequence label = (sensitive && !showingContent) ?
|
||||||
context.getString(R.string.status_sensitive_media_title) :
|
context.getString(R.string.post_sensitive_media_title) :
|
||||||
mediaDescriptions[index];
|
mediaDescriptions[index];
|
||||||
mediaLabels[index].setText(label);
|
mediaLabels[index].setText(label);
|
||||||
}
|
}
|
||||||
|
@ -687,7 +687,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
duration = formatDuration(attachment.getMeta().getDuration()) + " ";
|
duration = formatDuration(attachment.getMeta().getDuration()) + " ";
|
||||||
}
|
}
|
||||||
if (TextUtils.isEmpty(attachment.getDescription())) {
|
if (TextUtils.isEmpty(attachment.getDescription())) {
|
||||||
return duration + context.getString(R.string.description_status_media_no_description_placeholder);
|
return duration + context.getString(R.string.description_post_media_no_description_placeholder);
|
||||||
} else {
|
} else {
|
||||||
return duration + attachment.getDescription();
|
return duration + attachment.getDescription();
|
||||||
}
|
}
|
||||||
|
@ -935,9 +935,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions),
|
getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions),
|
||||||
getReblogDescription(context, status),
|
getReblogDescription(context, status),
|
||||||
status.getUsername(),
|
status.getUsername(),
|
||||||
actionable.getReblogged() ? context.getString(R.string.description_status_reblogged) : "",
|
actionable.getReblogged() ? context.getString(R.string.description_post_reblogged) : "",
|
||||||
actionable.getFavourited() ? context.getString(R.string.description_status_favourited) : "",
|
actionable.getFavourited() ? context.getString(R.string.description_post_favourited) : "",
|
||||||
actionable.getBookmarked() ? context.getString(R.string.description_status_bookmarked) : "",
|
actionable.getBookmarked() ? context.getString(R.string.description_post_bookmarked) : "",
|
||||||
getMediaDescription(context, status),
|
getMediaDescription(context, status),
|
||||||
getVisibilityDescription(context, actionable.getVisibility()),
|
getVisibilityDescription(context, actionable.getVisibility()),
|
||||||
getFavsText(context, actionable.getFavouritesCount()),
|
getFavsText(context, actionable.getFavouritesCount()),
|
||||||
|
@ -952,7 +952,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
Status reblog = status.getRebloggingStatus();
|
Status reblog = status.getRebloggingStatus();
|
||||||
if (reblog != null) {
|
if (reblog != null) {
|
||||||
return context
|
return context
|
||||||
.getString(R.string.status_boosted_format, reblog.getAccount().getUsername());
|
.getString(R.string.post_boosted_format, reblog.getAccount().getUsername());
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -969,20 +969,20 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
(builder, a) -> {
|
(builder, a) -> {
|
||||||
if (a.getDescription() == null) {
|
if (a.getDescription() == null) {
|
||||||
String placeholder =
|
String placeholder =
|
||||||
context.getString(R.string.description_status_media_no_description_placeholder);
|
context.getString(R.string.description_post_media_no_description_placeholder);
|
||||||
return builder.append(placeholder);
|
return builder.append(placeholder);
|
||||||
} else {
|
} else {
|
||||||
builder.append("; ");
|
builder.append("; ");
|
||||||
return builder.append(a.getDescription());
|
return builder.append(a.getDescription());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return context.getString(R.string.description_status_media, mediaDescriptions);
|
return context.getString(R.string.description_post_media, mediaDescriptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CharSequence getContentWarningDescription(Context context,
|
private static CharSequence getContentWarningDescription(Context context,
|
||||||
@NonNull StatusViewData.Concrete status) {
|
@NonNull StatusViewData.Concrete status) {
|
||||||
if (!TextUtils.isEmpty(status.getSpoilerText())) {
|
if (!TextUtils.isEmpty(status.getSpoilerText())) {
|
||||||
return context.getString(R.string.description_status_cw, status.getSpoilerText());
|
return context.getString(R.string.description_post_cw, status.getSpoilerText());
|
||||||
} else {
|
} else {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,7 +86,7 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
||||||
final StatusDisplayOptions statusDisplayOptions) {
|
final StatusDisplayOptions statusDisplayOptions) {
|
||||||
Context context = statusInfo.getContext();
|
Context context = statusInfo.getContext();
|
||||||
CharSequence wrappedName = StringUtils.unicodeWrap(name);
|
CharSequence wrappedName = StringUtils.unicodeWrap(name);
|
||||||
CharSequence boostedText = context.getString(R.string.status_boosted_format, wrappedName);
|
CharSequence boostedText = context.getString(R.string.post_boosted_format, wrappedName);
|
||||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
||||||
boostedText, accountEmoji, statusInfo, statusDisplayOptions.animateEmojis()
|
boostedText, accountEmoji, statusInfo, statusDisplayOptions.animateEmojis()
|
||||||
);
|
);
|
||||||
|
@ -118,10 +118,10 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
||||||
|
|
||||||
contentCollapseButton.setVisibility(View.VISIBLE);
|
contentCollapseButton.setVisibility(View.VISIBLE);
|
||||||
if (status.isCollapsed()) {
|
if (status.isCollapsed()) {
|
||||||
contentCollapseButton.setText(R.string.status_content_warning_show_more);
|
contentCollapseButton.setText(R.string.post_content_warning_show_more);
|
||||||
content.setFilters(COLLAPSE_INPUT_FILTER);
|
content.setFilters(COLLAPSE_INPUT_FILTER);
|
||||||
} else {
|
} else {
|
||||||
contentCollapseButton.setText(R.string.status_content_warning_show_less);
|
contentCollapseButton.setText(R.string.post_content_warning_show_less);
|
||||||
content.setFilters(NO_INPUT_FILTER);
|
content.setFilters(NO_INPUT_FILTER);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -239,7 +239,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
binding.accountFragmentViewPager.adapter = adapter
|
binding.accountFragmentViewPager.adapter = adapter
|
||||||
binding.accountFragmentViewPager.offscreenPageLimit = 2
|
binding.accountFragmentViewPager.offscreenPageLimit = 2
|
||||||
|
|
||||||
val pageTitles = arrayOf(getString(R.string.title_statuses), getString(R.string.title_statuses_with_replies), getString(R.string.title_statuses_pinned), getString(R.string.title_media))
|
val pageTitles = arrayOf(getString(R.string.title_posts), getString(R.string.title_posts_with_replies), getString(R.string.title_posts_pinned), getString(R.string.title_media))
|
||||||
|
|
||||||
TabLayoutMediator(binding.accountTabLayout, binding.accountFragmentViewPager) { tab, position ->
|
TabLayoutMediator(binding.accountTabLayout, binding.accountFragmentViewPager) { tab, position ->
|
||||||
tab.text = pageTitles[position]
|
tab.text = pageTitles[position]
|
||||||
|
@ -411,7 +411,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
private fun onAccountChanged(account: Account?) {
|
private fun onAccountChanged(account: Account?) {
|
||||||
loadedAccount = account ?: return
|
loadedAccount = account ?: return
|
||||||
|
|
||||||
val usernameFormatted = getString(R.string.status_username_format, account.username)
|
val usernameFormatted = getString(R.string.post_username_format, account.username)
|
||||||
binding.accountUsernameTextView.text = usernameFormatted
|
binding.accountUsernameTextView.text = usernameFormatted
|
||||||
binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis)
|
binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis)
|
||||||
|
|
||||||
|
@ -482,7 +482,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
supportActionBar?.title = emojifiedName
|
supportActionBar?.title = emojifiedName
|
||||||
}
|
}
|
||||||
supportActionBar?.subtitle = String.format(getString(R.string.status_username_format), account.username)
|
supportActionBar?.subtitle = String.format(getString(R.string.post_username_format), account.username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -499,7 +499,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.accountMovedDisplayName.text = movedAccount.name
|
binding.accountMovedDisplayName.text = movedAccount.name
|
||||||
binding.accountMovedUsername.text = getString(R.string.status_username_format, movedAccount.username)
|
binding.accountMovedUsername.text = getString(R.string.post_username_format, movedAccount.username)
|
||||||
|
|
||||||
val avatarRadius = resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
val avatarRadius = resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,9 @@
|
||||||
package com.keylesspalace.tusky.components.compose
|
package com.keylesspalace.tusky.components.compose
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.app.NotificationManager
|
||||||
import android.app.ProgressDialog
|
import android.app.ProgressDialog
|
||||||
|
import android.content.ClipData
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
@ -46,8 +48,8 @@ import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
import androidx.core.view.ContentInfoCompat
|
||||||
import androidx.core.view.inputmethod.InputContentInfoCompat
|
import androidx.core.view.OnReceiveContentListener
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.core.widget.doAfterTextChanged
|
import androidx.core.widget.doAfterTextChanged
|
||||||
|
@ -109,7 +111,7 @@ class ComposeActivity :
|
||||||
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
ComposeAutoCompleteAdapter.AutocompletionProvider,
|
||||||
OnEmojiSelectedListener,
|
OnEmojiSelectedListener,
|
||||||
Injectable,
|
Injectable,
|
||||||
InputConnectionCompat.OnCommitContentListener,
|
OnReceiveContentListener,
|
||||||
ComposeScheduleView.OnTimeSetListener {
|
ComposeScheduleView.OnTimeSetListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -155,6 +157,18 @@ class ComposeActivity :
|
||||||
public override fun onCreate(savedInstanceState: Bundle?) {
|
public override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val notificationId = intent.getIntExtra(NOTIFICATION_ID_EXTRA, -1)
|
||||||
|
if (notificationId != -1) {
|
||||||
|
// ComposeActivity was opened from a notification, delete the notification
|
||||||
|
val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.cancel(notificationId)
|
||||||
|
}
|
||||||
|
|
||||||
|
val accountId = intent.getLongExtra(ACCOUNT_ID_EXTRA, -1)
|
||||||
|
if (accountId != -1L) {
|
||||||
|
accountManager.setActiveAccount(accountId)
|
||||||
|
}
|
||||||
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
|
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
|
||||||
if (theme == "black") {
|
if (theme == "black") {
|
||||||
|
@ -194,9 +208,9 @@ class ComposeActivity :
|
||||||
viewModel.setup(composeOptions)
|
viewModel.setup(composeOptions)
|
||||||
setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent)
|
setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent)
|
||||||
setupQuoteView(composeOptions?.quoteStatusAuthor, composeOptions?.quoteStatusContent)
|
setupQuoteView(composeOptions?.quoteStatusAuthor, composeOptions?.quoteStatusContent)
|
||||||
val tootText = composeOptions?.tootText
|
val statusContent = composeOptions?.content
|
||||||
if (!tootText.isNullOrEmpty()) {
|
if (!statusContent.isNullOrEmpty()) {
|
||||||
binding.composeEditField.setText(tootText)
|
binding.composeEditField.setText(statusContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.loadInstanceDataFromNetwork(loadInstanceData(preferences, composeOptions?.tootRightNow == true))
|
viewModel.loadInstanceDataFromNetwork(loadInstanceData(preferences, composeOptions?.tootRightNow == true))
|
||||||
|
@ -249,26 +263,25 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (type == "text/plain" && intent.action == Intent.ACTION_SEND) {
|
}
|
||||||
|
|
||||||
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT)
|
||||||
val text = intent.getStringExtra(Intent.EXTRA_TEXT).orEmpty()
|
val text = intent.getStringExtra(Intent.EXTRA_TEXT).orEmpty()
|
||||||
val shareBody = if (!subject.isNullOrBlank() && subject !in text) {
|
val shareBody = if (!subject.isNullOrBlank() && subject !in text) {
|
||||||
subject + '\n' + text
|
subject + '\n' + text
|
||||||
} else {
|
} else {
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shareBody.isNotBlank()) {
|
if (shareBody.isNotBlank()) {
|
||||||
val start = binding.composeEditField.selectionStart.coerceAtLeast(0)
|
val start = binding.composeEditField.selectionStart.coerceAtLeast(0)
|
||||||
val end = binding.composeEditField.selectionEnd.coerceAtLeast(0)
|
val end = binding.composeEditField.selectionEnd.coerceAtLeast(0)
|
||||||
val left = min(start, end)
|
val left = min(start, end)
|
||||||
val right = max(start, end)
|
val right = max(start, end)
|
||||||
binding.composeEditField.text.replace(left, right, shareBody, 0, shareBody.length)
|
binding.composeEditField.text.replace(left, right, shareBody, 0, shareBody.length)
|
||||||
// move edittext cursor to first when shareBody parsed
|
// move edittext cursor to first when shareBody parsed
|
||||||
binding.composeEditField.text.insert(0, "\n")
|
binding.composeEditField.text.insert(0, "\n")
|
||||||
binding.composeEditField.setSelection(0)
|
binding.composeEditField.setSelection(0)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -336,7 +349,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
|
private fun setupComposeField(preferences: SharedPreferences, startingText: String?) {
|
||||||
binding.composeEditField.setOnCommitContentListener(this)
|
binding.composeEditField.setOnReceiveContentListener(this)
|
||||||
|
|
||||||
binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) }
|
binding.composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) }
|
||||||
|
|
||||||
|
@ -776,7 +789,9 @@ class ComposeActivity :
|
||||||
val urlSpans = binding.composeEditField.urls
|
val urlSpans = binding.composeEditField.urls
|
||||||
if (urlSpans != null) {
|
if (urlSpans != null) {
|
||||||
for (span in urlSpans) {
|
for (span in urlSpans) {
|
||||||
offset += max(0, span.url.length - charactersReservedPerUrl)
|
// it's expected that this will be negative
|
||||||
|
// when the url length is less than the reserved character count
|
||||||
|
offset += (span.url.length - charactersReservedPerUrl)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var length = binding.composeEditField.length() - offset
|
var length = binding.composeEditField.length() - offset
|
||||||
|
@ -819,26 +834,18 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** This is for the fancy keyboards which can insert images and stuff. */
|
/** This is for the fancy keyboards which can insert images and stuff, and drag&drop etc */
|
||||||
override fun onCommitContent(inputContentInfo: InputContentInfoCompat, flags: Int, opts: Bundle?): Boolean {
|
override fun onReceiveContent(view: View, contentInfo: ContentInfoCompat): ContentInfoCompat? {
|
||||||
// Verify the returned content's type is of the correct MIME type
|
if (contentInfo.clip.description.hasMimeType("image/*")) {
|
||||||
val supported = inputContentInfo.description.hasMimeType("image/*")
|
val split = contentInfo.partition { item: ClipData.Item -> item.uri != null }
|
||||||
|
split.first?.let { content ->
|
||||||
if (supported) {
|
for (i in 0 until content.clip.itemCount) {
|
||||||
val lacksPermission = (flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0
|
pickMedia(content.clip.getItemAt(i).uri)
|
||||||
if (lacksPermission) {
|
|
||||||
try {
|
|
||||||
inputContentInfo.requestPermission()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "InputContentInfoCompat#requestPermission() failed." + e.message)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pickMedia(inputContentInfo.contentUri, inputContentInfo)
|
return split.second
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
return contentInfo
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendStatus() {
|
private fun sendStatus() {
|
||||||
|
@ -865,12 +872,11 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.sendStatus(contentText, spoilerText).observe(
|
viewModel.sendStatus(contentText, spoilerText).observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
finishingUploadDialog?.dismiss()
|
finishingUploadDialog?.dismiss()
|
||||||
deleteDraftAndFinish()
|
deleteDraftAndFinish()
|
||||||
}
|
}
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
binding.composeEditField.error = getString(R.string.error_compose_character_limit)
|
binding.composeEditField.error = getString(R.string.error_compose_character_limit)
|
||||||
enableButtons(true)
|
enableButtons(true)
|
||||||
|
@ -940,12 +946,9 @@ class ComposeActivity :
|
||||||
viewModel.removeMediaFromQueue(item)
|
viewModel.removeMediaFromQueue(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pickMedia(uri: Uri, contentInfoCompat: InputContentInfoCompat? = null) {
|
private fun pickMedia(uri: Uri) {
|
||||||
withLifecycleContext {
|
withLifecycleContext {
|
||||||
viewModel.pickMedia(uri).observe { exceptionOrItem ->
|
viewModel.pickMedia(uri).observe { exceptionOrItem ->
|
||||||
|
|
||||||
contentInfoCompat?.releasePermission()
|
|
||||||
|
|
||||||
exceptionOrItem.asLeftOrNull()?.let {
|
exceptionOrItem.asLeftOrNull()?.let {
|
||||||
val errorId = when (it) {
|
val errorId = when (it) {
|
||||||
is VideoSizeException -> {
|
is VideoSizeException -> {
|
||||||
|
@ -1098,29 +1101,29 @@ class ComposeActivity :
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ComposeOptions(
|
data class ComposeOptions(
|
||||||
// Let's keep fields var until all consumers are Kotlin
|
// Let's keep fields var until all consumers are Kotlin
|
||||||
var scheduledTootId: String? = null,
|
var scheduledTootId: String? = null,
|
||||||
var draftId: Int? = null,
|
var draftId: Int? = null,
|
||||||
var tootText: String? = null,
|
var content: String? = null,
|
||||||
var mediaUrls: List<String>? = null,
|
var mediaUrls: List<String>? = null,
|
||||||
var mediaDescriptions: List<String>? = null,
|
var mediaDescriptions: List<String>? = null,
|
||||||
var mentionedUsernames: Set<String>? = null,
|
var mentionedUsernames: Set<String>? = null,
|
||||||
var inReplyToId: String? = null,
|
var inReplyToId: String? = null,
|
||||||
var quoteId: String? = null,
|
var quoteId: String? = null,
|
||||||
var quoteStatusAuthor: String? = null,
|
var quoteStatusAuthor: String? = null,
|
||||||
var quoteStatusContent: String? = null,
|
var quoteStatusContent: String? = null,
|
||||||
var replyVisibility: Status.Visibility? = null,
|
var replyVisibility: Status.Visibility? = null,
|
||||||
var visibility: Status.Visibility? = null,
|
var visibility: Status.Visibility? = null,
|
||||||
var contentWarning: String? = null,
|
var contentWarning: String? = null,
|
||||||
var replyingStatusAuthor: String? = null,
|
var replyingStatusAuthor: String? = null,
|
||||||
var replyingStatusContent: String? = null,
|
var replyingStatusContent: String? = null,
|
||||||
var mediaAttachments: List<Attachment>? = null,
|
var mediaAttachments: List<Attachment>? = null,
|
||||||
var draftAttachments: List<DraftAttachment>? = null,
|
var draftAttachments: List<DraftAttachment>? = null,
|
||||||
var scheduledAt: String? = null,
|
var scheduledAt: String? = null,
|
||||||
var sensitive: Boolean? = null,
|
var sensitive: Boolean? = null,
|
||||||
var poll: NewPoll? = null,
|
var poll: NewPoll? = null,
|
||||||
var modifiedInitialState: Boolean? = null,
|
var modifiedInitialState: Boolean? = null,
|
||||||
var tootRightNow: Boolean? = null
|
var tootRightNow: Boolean? = null,
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -1128,6 +1131,8 @@ class ComposeActivity :
|
||||||
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
||||||
|
|
||||||
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS"
|
||||||
|
private const val NOTIFICATION_ID_EXTRA = "NOTIFICATION_ID"
|
||||||
|
private const val ACCOUNT_ID_EXTRA = "ACCOUNT_ID"
|
||||||
private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI"
|
private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI"
|
||||||
|
|
||||||
@JvmField
|
@JvmField
|
||||||
|
@ -1136,10 +1141,28 @@ class ComposeActivity :
|
||||||
const val PREF_DEFAULT_TAG = "default_tag"
|
const val PREF_DEFAULT_TAG = "default_tag"
|
||||||
const val PREF_USE_DEFAULT_TAG = "use_default_tag"
|
const val PREF_USE_DEFAULT_TAG = "use_default_tag"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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
|
@JvmStatic
|
||||||
fun startIntent(context: Context, options: ComposeOptions): Intent {
|
@JvmOverloads
|
||||||
|
fun startIntent(
|
||||||
|
context: Context,
|
||||||
|
options: ComposeOptions,
|
||||||
|
notificationId: Int? = null,
|
||||||
|
accountId: Long? = null
|
||||||
|
): Intent {
|
||||||
return Intent(context, ComposeActivity::class.java).apply {
|
return Intent(context, ComposeActivity::class.java).apply {
|
||||||
putExtra(COMPOSE_OPTIONS_EXTRA, options)
|
putExtra(COMPOSE_OPTIONS_EXTRA, options)
|
||||||
|
if (notificationId != null) {
|
||||||
|
putExtra(NOTIFICATION_ID_EXTRA, notificationId)
|
||||||
|
}
|
||||||
|
if (accountId != null) {
|
||||||
|
putExtra(ACCOUNT_ID_EXTRA, accountId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@
|
||||||
package com.keylesspalace.tusky.components.compose;
|
package com.keylesspalace.tusky.components.compose;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
@ -28,9 +27,9 @@ import android.widget.TextView;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
|
||||||
import com.keylesspalace.tusky.entity.Emoji;
|
import com.keylesspalace.tusky.entity.Emoji;
|
||||||
import com.keylesspalace.tusky.entity.HashTag;
|
import com.keylesspalace.tusky.entity.HashTag;
|
||||||
|
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||||
|
|
||||||
|
@ -144,9 +143,9 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter
|
||||||
|
|
||||||
AccountResult accountResult = ((AccountResult) getItem(position));
|
AccountResult accountResult = ((AccountResult) getItem(position));
|
||||||
if (accountResult != null) {
|
if (accountResult != null) {
|
||||||
Account account = accountResult.account;
|
TimelineAccount account = accountResult.account;
|
||||||
String formattedUsername = context.getString(
|
String formattedUsername = context.getString(
|
||||||
R.string.status_username_format,
|
R.string.post_username_format,
|
||||||
account.getUsername()
|
account.getUsername()
|
||||||
);
|
);
|
||||||
accountViewHolder.username.setText(formattedUsername);
|
accountViewHolder.username.setText(formattedUsername);
|
||||||
|
@ -268,9 +267,9 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
public final static class AccountResult extends AutocompleteResult {
|
public final static class AccountResult extends AutocompleteResult {
|
||||||
private final Account account;
|
private final TimelineAccount account;
|
||||||
|
|
||||||
public AccountResult(Account account) {
|
public AccountResult(TimelineAccount account) {
|
||||||
this.account = account;
|
this.account = account;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ import com.keylesspalace.tusky.entity.NewPoll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.service.ServiceClient
|
import com.keylesspalace.tusky.service.ServiceClient
|
||||||
import com.keylesspalace.tusky.service.TootToSend
|
import com.keylesspalace.tusky.service.StatusToSend
|
||||||
import com.keylesspalace.tusky.util.Either
|
import com.keylesspalace.tusky.util.Either
|
||||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||||
import com.keylesspalace.tusky.util.VersionUtils
|
import com.keylesspalace.tusky.util.VersionUtils
|
||||||
|
@ -315,25 +315,25 @@ class ComposeViewModel @Inject constructor(
|
||||||
mediaDescriptions.add(item.description ?: "")
|
mediaDescriptions.add(item.description ?: "")
|
||||||
}
|
}
|
||||||
|
|
||||||
val tootToSend = TootToSend(
|
val tootToSend = StatusToSend(
|
||||||
text = content,
|
text = content,
|
||||||
warningText = spoilerText,
|
warningText = spoilerText,
|
||||||
visibility = statusVisibility.value!!.serverString(),
|
visibility = statusVisibility.value!!.serverString(),
|
||||||
sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
|
sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
|
||||||
mediaIds = mediaIds,
|
mediaIds = mediaIds,
|
||||||
mediaUris = mediaUris.map { it.toString() },
|
mediaUris = mediaUris.map { it.toString() },
|
||||||
mediaDescriptions = mediaDescriptions,
|
mediaDescriptions = mediaDescriptions,
|
||||||
scheduledAt = scheduledAt.value,
|
scheduledAt = scheduledAt.value,
|
||||||
inReplyToId = inReplyToId,
|
inReplyToId = inReplyToId,
|
||||||
poll = poll.value,
|
poll = poll.value,
|
||||||
replyingStatusContent = null,
|
replyingStatusContent = null,
|
||||||
replyingStatusAuthorUsername = null,
|
replyingStatusAuthorUsername = null,
|
||||||
quoteId = quoteId,
|
quoteId = quoteId,
|
||||||
accountId = accountManager.activeAccount!!.id,
|
accountId = accountManager.activeAccount!!.id,
|
||||||
draftId = draftId,
|
draftId = draftId,
|
||||||
idempotencyKey = randomAlphanumericString(16),
|
idempotencyKey = randomAlphanumericString(16),
|
||||||
retries = 0
|
retries = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
serviceClient.sendToot(tootToSend)
|
serviceClient.sendToot(tootToSend)
|
||||||
}
|
}
|
||||||
|
@ -468,7 +468,7 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
draftId = composeOptions?.draftId ?: 0
|
draftId = composeOptions?.draftId ?: 0
|
||||||
scheduledTootId = composeOptions?.scheduledTootId
|
scheduledTootId = composeOptions?.scheduledTootId
|
||||||
startingText = composeOptions?.tootText
|
startingText = composeOptions?.content
|
||||||
|
|
||||||
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
|
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
|
||||||
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
|
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.compose
|
package com.keylesspalace.tusky.components.compose
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
@ -37,6 +38,7 @@ import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
@ -83,36 +85,70 @@ class MediaUploader @Inject constructor(
|
||||||
|
|
||||||
fun prepareMedia(inUri: Uri): Single<PreparedMedia> {
|
fun prepareMedia(inUri: Uri): Single<PreparedMedia> {
|
||||||
return Single.fromCallable {
|
return Single.fromCallable {
|
||||||
var mediaSize = getMediaSize(contentResolver, inUri)
|
var mediaSize = MEDIA_SIZE_UNKNOWN
|
||||||
var uri = inUri
|
var uri = inUri
|
||||||
val mimeType = contentResolver.getType(uri)
|
var mimeType: String? = null
|
||||||
|
|
||||||
val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
contentResolver.openInputStream(inUri).use { input ->
|
when (inUri.scheme) {
|
||||||
if (input == null) {
|
ContentResolver.SCHEME_CONTENT -> {
|
||||||
Log.w(TAG, "Media input is null")
|
|
||||||
uri = inUri
|
mimeType = contentResolver.getType(uri)
|
||||||
return@use
|
|
||||||
|
val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
|
||||||
|
|
||||||
|
contentResolver.openInputStream(inUri).use { input ->
|
||||||
|
if (input == null) {
|
||||||
|
Log.w(TAG, "Media input is null")
|
||||||
|
uri = inUri
|
||||||
|
return@use
|
||||||
|
}
|
||||||
|
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
|
||||||
|
FileOutputStream(file.absoluteFile).use { out ->
|
||||||
|
input.copyTo(out)
|
||||||
|
uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
mediaSize = getMediaSize(contentResolver, uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
|
ContentResolver.SCHEME_FILE -> {
|
||||||
FileOutputStream(file.absoluteFile).use { out ->
|
val path = uri.path
|
||||||
input.copyTo(out)
|
if (path == null) {
|
||||||
uri = FileProvider.getUriForFile(
|
Log.w(TAG, "empty uri path $uri")
|
||||||
context,
|
throw CouldNotOpenFileException()
|
||||||
BuildConfig.APPLICATION_ID + ".fileprovider",
|
}
|
||||||
file
|
val inputFile = File(path)
|
||||||
)
|
val suffix = inputFile.name.substringAfterLast('.', "tmp")
|
||||||
mediaSize = getMediaSize(contentResolver, uri)
|
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix)
|
||||||
|
val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir)
|
||||||
|
val input = FileInputStream(inputFile)
|
||||||
|
|
||||||
|
FileOutputStream(file.absoluteFile).use { out ->
|
||||||
|
input.copyTo(out)
|
||||||
|
uri = FileProvider.getUriForFile(
|
||||||
|
context,
|
||||||
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
|
file
|
||||||
|
)
|
||||||
|
mediaSize = getMediaSize(contentResolver, uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.w(TAG, "Unknown uri scheme $uri")
|
||||||
|
throw CouldNotOpenFileException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
Log.w(TAG, e)
|
Log.w(TAG, e)
|
||||||
uri = inUri
|
throw CouldNotOpenFileException()
|
||||||
}
|
}
|
||||||
if (mediaSize == MEDIA_SIZE_UNKNOWN) {
|
if (mediaSize == MEDIA_SIZE_UNKNOWN) {
|
||||||
throw CouldNotOpenFileException()
|
Log.w(TAG, "Could not determine file size of upload")
|
||||||
|
throw MediaTypeException()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mimeType != null) {
|
if (mimeType != null) {
|
||||||
|
@ -138,6 +174,7 @@ class MediaUploader @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
Log.w(TAG, "Could not determine mime type of upload")
|
||||||
throw MediaTypeException()
|
throw MediaTypeException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,8 @@ import android.util.AttributeSet
|
||||||
import android.view.inputmethod.EditorInfo
|
import android.view.inputmethod.EditorInfo
|
||||||
import android.view.inputmethod.InputConnection
|
import android.view.inputmethod.InputConnection
|
||||||
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView
|
import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView
|
||||||
|
import androidx.core.view.OnReceiveContentListener
|
||||||
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||||
import androidx.emoji.widget.EmojiEditTextHelper
|
import androidx.emoji.widget.EmojiEditTextHelper
|
||||||
|
@ -32,41 +34,33 @@ class EditTextTyped @JvmOverloads constructor(
|
||||||
) :
|
) :
|
||||||
AppCompatMultiAutoCompleteTextView(context, attributeSet) {
|
AppCompatMultiAutoCompleteTextView(context, attributeSet) {
|
||||||
|
|
||||||
private var onCommitContentListener: InputConnectionCompat.OnCommitContentListener? = null
|
|
||||||
private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this)
|
private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// fix a bug with autocomplete and some keyboards
|
// fix a bug with autocomplete and some keyboards
|
||||||
val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)
|
val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE)
|
||||||
inputType = newInputType
|
inputType = newInputType
|
||||||
super.setKeyListener(getEmojiEditTextHelper().getKeyListener(keyListener))
|
super.setKeyListener(emojiEditTextHelper.getKeyListener(keyListener))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun setKeyListener(input: KeyListener) {
|
override fun setKeyListener(input: KeyListener?) {
|
||||||
super.setKeyListener(getEmojiEditTextHelper().getKeyListener(input))
|
if (input != null) {
|
||||||
|
super.setKeyListener(emojiEditTextHelper.getKeyListener(input))
|
||||||
|
} else {
|
||||||
|
super.setKeyListener(input)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setOnCommitContentListener(listener: InputConnectionCompat.OnCommitContentListener) {
|
fun setOnReceiveContentListener(listener: OnReceiveContentListener) {
|
||||||
onCommitContentListener = listener
|
ViewCompat.setOnReceiveContentListener(this, arrayOf("image/*"), listener)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection {
|
||||||
val connection = super.onCreateInputConnection(editorInfo)
|
val connection = super.onCreateInputConnection(editorInfo)
|
||||||
return if (onCommitContentListener != null) {
|
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
||||||
EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*"))
|
return emojiEditTextHelper.onCreateInputConnection(
|
||||||
getEmojiEditTextHelper().onCreateInputConnection(
|
InputConnectionCompat.createWrapper(this, connection, editorInfo),
|
||||||
InputConnectionCompat.createWrapper(
|
editorInfo
|
||||||
connection, editorInfo,
|
)!!
|
||||||
onCommitContentListener!!
|
|
||||||
),
|
|
||||||
editorInfo
|
|
||||||
)!!
|
|
||||||
} else {
|
|
||||||
connection
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getEmojiEditTextHelper(): EmojiEditTextHelper {
|
|
||||||
return emojiEditTextHelper
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,18 +16,17 @@
|
||||||
package com.keylesspalace.tusky.components.conversation
|
package com.keylesspalace.tusky.components.conversation
|
||||||
|
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.text.SpannedString
|
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import com.keylesspalace.tusky.db.Converters
|
import com.keylesspalace.tusky.db.Converters
|
||||||
import com.keylesspalace.tusky.entity.Account
|
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.entity.Conversation
|
import com.keylesspalace.tusky.entity.Conversation
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
import com.keylesspalace.tusky.entity.HashTag
|
import com.keylesspalace.tusky.entity.HashTag
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
@ -48,17 +47,15 @@ data class ConversationAccountEntity(
|
||||||
val avatar: String,
|
val avatar: String,
|
||||||
val emojis: List<Emoji>
|
val emojis: List<Emoji>
|
||||||
) {
|
) {
|
||||||
fun toAccount(): Account {
|
fun toAccount(): TimelineAccount {
|
||||||
return Account(
|
return TimelineAccount(
|
||||||
id = id,
|
id = id,
|
||||||
username = username,
|
username = username,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
|
url = "",
|
||||||
avatar = avatar,
|
avatar = avatar,
|
||||||
emojis = emojis,
|
emojis = emojis,
|
||||||
url = "",
|
|
||||||
localUsername = "",
|
localUsername = "",
|
||||||
note = SpannedString(""),
|
|
||||||
header = ""
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,7 +97,7 @@ data class ConversationStatusEntity(
|
||||||
if (inReplyToId != other.inReplyToId) return false
|
if (inReplyToId != other.inReplyToId) return false
|
||||||
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
||||||
if (account != other.account) return false
|
if (account != other.account) return false
|
||||||
if (content.toString() != other.content.toString()) return false // TODO find a better method to compare two spanned strings
|
if (content.toString() != other.content.toString()) return false
|
||||||
if (createdAt != other.createdAt) return false
|
if (createdAt != other.createdAt) return false
|
||||||
if (emojis != other.emojis) return false
|
if (emojis != other.emojis) return false
|
||||||
if (favouritesCount != other.favouritesCount) return false
|
if (favouritesCount != other.favouritesCount) return false
|
||||||
|
@ -126,7 +123,7 @@ data class ConversationStatusEntity(
|
||||||
result = 31 * result + (inReplyToId?.hashCode() ?: 0)
|
result = 31 * result + (inReplyToId?.hashCode() ?: 0)
|
||||||
result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0)
|
result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0)
|
||||||
result = 31 * result + account.hashCode()
|
result = 31 * result + account.hashCode()
|
||||||
result = 31 * result + content.hashCode()
|
result = 31 * result + content.toString().hashCode()
|
||||||
result = 31 * result + createdAt.hashCode()
|
result = 31 * result + createdAt.hashCode()
|
||||||
result = 31 * result + emojis.hashCode()
|
result = 31 * result + emojis.hashCode()
|
||||||
result = 31 * result + favouritesCount
|
result = 31 * result + favouritesCount
|
||||||
|
@ -177,7 +174,7 @@ data class ConversationStatusEntity(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Account.toEntity() =
|
fun TimelineAccount.toEntity() =
|
||||||
ConversationAccountEntity(
|
ConversationAccountEntity(
|
||||||
id = id,
|
id = id,
|
||||||
username = username,
|
username = username,
|
||||||
|
|
|
@ -154,10 +154,10 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
|
|
||||||
contentCollapseButton.setVisibility(View.VISIBLE);
|
contentCollapseButton.setVisibility(View.VISIBLE);
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
contentCollapseButton.setText(R.string.status_content_warning_show_more);
|
contentCollapseButton.setText(R.string.post_content_warning_show_more);
|
||||||
content.setFilters(COLLAPSE_INPUT_FILTER);
|
content.setFilters(COLLAPSE_INPUT_FILTER);
|
||||||
} else {
|
} else {
|
||||||
contentCollapseButton.setText(R.string.status_content_warning_show_less);
|
contentCollapseButton.setText(R.string.post_content_warning_show_less);
|
||||||
content.setFilters(NO_INPUT_FILTER);
|
content.setFilters(NO_INPUT_FILTER);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -90,14 +90,14 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
|
|
||||||
if (draft.inReplyToId != null) {
|
if (draft.inReplyToId != null) {
|
||||||
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
|
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
viewModel.getToot(draft.inReplyToId)
|
viewModel.getStatus(draft.inReplyToId)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.autoDispose(from(this))
|
.autoDispose(from(this))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{ status ->
|
{ status ->
|
||||||
val composeOptions = ComposeActivity.ComposeOptions(
|
val composeOptions = ComposeActivity.ComposeOptions(
|
||||||
draftId = draft.id,
|
draftId = draft.id,
|
||||||
tootText = draft.content,
|
content = draft.content,
|
||||||
contentWarning = draft.contentWarning,
|
contentWarning = draft.contentWarning,
|
||||||
inReplyToId = draft.inReplyToId,
|
inReplyToId = draft.inReplyToId,
|
||||||
replyingStatusContent = status.content.toString(),
|
replyingStatusContent = status.content.toString(),
|
||||||
|
@ -121,7 +121,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
if (throwable is HttpException && throwable.code() == 404) {
|
if (throwable is HttpException && throwable.code() == 404) {
|
||||||
// the original status to which a reply was drafted has been deleted
|
// the original status to which a reply was drafted has been deleted
|
||||||
// let's open the ComposeActivity without reply information
|
// let's open the ComposeActivity without reply information
|
||||||
Toast.makeText(this, getString(R.string.drafts_toot_reply_removed), Toast.LENGTH_LONG).show()
|
Toast.makeText(this, getString(R.string.drafts_post_reply_removed), Toast.LENGTH_LONG).show()
|
||||||
openDraftWithoutReply(draft)
|
openDraftWithoutReply(draft)
|
||||||
} else {
|
} else {
|
||||||
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
|
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
|
||||||
|
@ -137,7 +137,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
private fun openDraftWithoutReply(draft: DraftEntity) {
|
private fun openDraftWithoutReply(draft: DraftEntity) {
|
||||||
val composeOptions = ComposeActivity.ComposeOptions(
|
val composeOptions = ComposeActivity.ComposeOptions(
|
||||||
draftId = draft.id,
|
draftId = draft.id,
|
||||||
tootText = draft.content,
|
content = draft.content,
|
||||||
contentWarning = draft.contentWarning,
|
contentWarning = draft.contentWarning,
|
||||||
draftAttachments = draft.attachments,
|
draftAttachments = draft.attachments,
|
||||||
poll = draft.poll,
|
poll = draft.poll,
|
||||||
|
|
|
@ -60,8 +60,8 @@ class DraftsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getToot(tootId: String): Single<Status> {
|
fun getStatus(statusId: String): Single<Status> {
|
||||||
return api.status(tootId)
|
return api.status(statusId)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
|
|
|
@ -0,0 +1,288 @@
|
||||||
|
/* 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.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.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.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val credentials: AppCredentials = try {
|
||||||
|
mastodonApi.authenticateApp(
|
||||||
|
domain, getString(R.string.app_name), oauthRedirectUri,
|
||||||
|
OAUTH_SCOPES, getString(R.string.tusky_website)
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
binding.loginButton.isEnabled = true
|
||||||
|
binding.domainTextInputLayout.error =
|
||||||
|
getString(R.string.error_failed_app_registration)
|
||||||
|
setLoading(false)
|
||||||
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before we open browser page we save the data.
|
||||||
|
// Even if we don't open other apps user may go to password manager or somewhere else
|
||||||
|
// and we will need to pick up the process where we left off.
|
||||||
|
// Alternatively we could pass it all as part of the intent and receive it back
|
||||||
|
// but it is a bit of a workaround.
|
||||||
|
preferences.edit()
|
||||||
|
.putString(DOMAIN, domain)
|
||||||
|
.putString(CLIENT_ID, credentials.clientId)
|
||||||
|
.putString(CLIENT_SECRET, credentials.clientSecret)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) {
|
||||||
|
// To authorize this app and log in it's necessary to redirect to the domain given,
|
||||||
|
// login there, and the server will redirect back to the app with its response.
|
||||||
|
val url = HttpUrl.Builder()
|
||||||
|
.scheme("https")
|
||||||
|
.host(domain)
|
||||||
|
.addPathSegments(MastodonApi.ENDPOINT_AUTHORIZE)
|
||||||
|
.addQueryParameter("client_id", clientId)
|
||||||
|
.addQueryParameter("redirect_uri", oauthRedirectUri)
|
||||||
|
.addQueryParameter("response_type", "code")
|
||||||
|
.addQueryParameter("scope", OAUTH_SCOPES)
|
||||||
|
.build()
|
||||||
|
doWebViewAuth.launch(LoginData(url.toString().toUri(), oauthRedirectUri.toUri()))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
// first show or user cancelled login
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchOauthToken(code: String) {
|
||||||
|
/* restore variables from SharedPreferences */
|
||||||
|
val domain = preferences.getNonNullString(DOMAIN, "")
|
||||||
|
val clientId = preferences.getNonNullString(CLIENT_ID, "")
|
||||||
|
val clientSecret = preferences.getNonNullString(CLIENT_SECRET, "")
|
||||||
|
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
val accessToken = try {
|
||||||
|
mastodonApi.fetchOAuthToken(
|
||||||
|
domain, clientId, clientSecret, oauthRedirectUri, code,
|
||||||
|
"authorization_code"
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
setLoading(false)
|
||||||
|
binding.domainTextInputLayout.error =
|
||||||
|
getString(R.string.error_retrieving_oauth_token)
|
||||||
|
Log.e(
|
||||||
|
TAG,
|
||||||
|
"%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
accountManager.addAccount(accessToken.accessToken, domain)
|
||||||
|
|
||||||
|
val intent = Intent(this, MainActivity::class.java)
|
||||||
|
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
startActivity(intent)
|
||||||
|
finish()
|
||||||
|
overridePendingTransition(R.anim.explode, R.anim.explode)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLoading(loadingState: Boolean) {
|
||||||
|
if (loadingState) {
|
||||||
|
binding.loginLoadingLayout.visibility = View.VISIBLE
|
||||||
|
binding.loginInputLayout.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
binding.loginLoadingLayout.visibility = View.GONE
|
||||||
|
binding.loginInputLayout.visibility = View.VISIBLE
|
||||||
|
binding.loginButton.isEnabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isAdditionalLogin(): Boolean {
|
||||||
|
return intent.getBooleanExtra(LOGIN_MODE, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "LoginActivity" // logging tag
|
||||||
|
private const val OAUTH_SCOPES = "read write follow"
|
||||||
|
private const val LOGIN_MODE = "LOGIN_MODE"
|
||||||
|
private const val DOMAIN = "domain"
|
||||||
|
private const val CLIENT_ID = "clientId"
|
||||||
|
private const val CLIENT_SECRET = "clientSecret"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun getIntent(context: Context, mode: Boolean): Intent {
|
||||||
|
val loginIntent = Intent(context, LoginActivity::class.java)
|
||||||
|
loginIntent.putExtra(LOGIN_MODE, mode)
|
||||||
|
return loginIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Make sure the user-entered text is just a fully-qualified domain name. */
|
||||||
|
private fun canonicalizeDomain(domain: String): String {
|
||||||
|
// Strip any schemes out.
|
||||||
|
var s = domain.replaceFirst("http://", "")
|
||||||
|
s = s.replaceFirst("https://", "")
|
||||||
|
// If a username was included (e.g. username@example.com), just take what's after the '@'.
|
||||||
|
val at = s.lastIndexOf('@')
|
||||||
|
if (at != -1) {
|
||||||
|
s = s.substring(at + 1)
|
||||||
|
}
|
||||||
|
return s.trim { it <= ' ' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
package com.keylesspalace.tusky.components.login
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.CookieManager
|
||||||
|
import android.webkit.WebResourceError
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebStorage
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
|
import com.keylesspalace.tusky.databinding.LoginWebviewBinding
|
||||||
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
/** Contract for starting [LoginWebViewActivity]. */
|
||||||
|
class OauthLogin : ActivityResultContract<LoginData, LoginResult>() {
|
||||||
|
override fun createIntent(context: Context, input: LoginData): Intent {
|
||||||
|
val intent = Intent(context, LoginWebViewActivity::class.java)
|
||||||
|
intent.putExtra(DATA_EXTRA, input)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseResult(resultCode: Int, intent: Intent?): LoginResult {
|
||||||
|
// Can happen automatically on up or back press
|
||||||
|
return if (resultCode == Activity.RESULT_CANCELED) {
|
||||||
|
LoginResult.Cancel
|
||||||
|
} else {
|
||||||
|
intent!!.getParcelableExtra(RESULT_EXTRA)!!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val RESULT_EXTRA = "result"
|
||||||
|
private const val DATA_EXTRA = "data"
|
||||||
|
|
||||||
|
fun parseData(intent: Intent): LoginData {
|
||||||
|
return intent.getParcelableExtra(DATA_EXTRA)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun makeResultIntent(result: LoginResult): Intent {
|
||||||
|
val intent = Intent()
|
||||||
|
intent.putExtra(RESULT_EXTRA, result)
|
||||||
|
return intent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class LoginData(
|
||||||
|
val url: Uri,
|
||||||
|
val oauthRedirectUrl: Uri,
|
||||||
|
) : Parcelable
|
||||||
|
|
||||||
|
sealed class LoginResult : Parcelable {
|
||||||
|
@Parcelize
|
||||||
|
data class Ok(val code: String) : LoginResult()
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class Err(val errorMessage: String) : LoginResult()
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object Cancel : LoginResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Activity to do Oauth process using WebView. */
|
||||||
|
class LoginWebViewActivity : BaseActivity(), Injectable {
|
||||||
|
private val binding by viewBinding(LoginWebviewBinding::inflate)
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
val data = OauthLogin.parseData(intent)
|
||||||
|
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
setSupportActionBar(binding.loginToolbar)
|
||||||
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
|
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||||
|
|
||||||
|
val webView = binding.loginWebView
|
||||||
|
webView.settings.allowContentAccess = false
|
||||||
|
webView.settings.allowFileAccess = false
|
||||||
|
webView.settings.databaseEnabled = false
|
||||||
|
webView.settings.displayZoomControls = false
|
||||||
|
webView.settings.javaScriptCanOpenWindowsAutomatically = false
|
||||||
|
// Javascript needs to be enabled because otherwise 2FA does not work in some instances
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
webView.settings.javaScriptEnabled = true
|
||||||
|
webView.settings.userAgentString += " Tusky/${BuildConfig.VERSION_NAME}"
|
||||||
|
|
||||||
|
val oauthUrl = data.oauthRedirectUrl
|
||||||
|
|
||||||
|
webView.webViewClient = object : WebViewClient() {
|
||||||
|
override fun onReceivedError(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest?,
|
||||||
|
error: WebResourceError
|
||||||
|
) {
|
||||||
|
Log.d("LoginWeb", "Failed to load ${data.url}: $error")
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldOverrideUrlLoading(
|
||||||
|
view: WebView,
|
||||||
|
request: WebResourceRequest
|
||||||
|
): Boolean {
|
||||||
|
val url = request.url
|
||||||
|
return if (url.scheme == oauthUrl.scheme && url.host == oauthUrl.host) {
|
||||||
|
val error = url.getQueryParameter("error")
|
||||||
|
if (error != null) {
|
||||||
|
sendResult(LoginResult.Err(error))
|
||||||
|
} else {
|
||||||
|
val code = url.getQueryParameter("code").orEmpty()
|
||||||
|
sendResult(LoginResult.Ok(code))
|
||||||
|
}
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
webView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
|
if (savedInstanceState == null) {
|
||||||
|
webView.loadUrl(data.url.toString())
|
||||||
|
} else {
|
||||||
|
webView.restoreState(savedInstanceState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
super.onSaveInstanceState(outState)
|
||||||
|
binding.loginWebView.saveState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
if (isFinishing) {
|
||||||
|
// We don't want to keep user session in WebView, we just want our own accessToken
|
||||||
|
WebStorage.getInstance().deleteAllData()
|
||||||
|
CookieManager.getInstance().removeAllCookies(null)
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun requiresLogin() = false
|
||||||
|
|
||||||
|
private fun sendResult(result: LoginResult) {
|
||||||
|
setResult(Activity.RESULT_OK, OauthLogin.makeResultIntent(result))
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,7 +24,6 @@ import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.graphics.Color;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
@ -46,9 +45,9 @@ import androidx.work.WorkRequest;
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||||
import com.bumptech.glide.request.FutureTarget;
|
import com.bumptech.glide.request.FutureTarget;
|
||||||
import com.keylesspalace.tusky.BuildConfig;
|
|
||||||
import com.keylesspalace.tusky.MainActivity;
|
import com.keylesspalace.tusky.MainActivity;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||||
import com.keylesspalace.tusky.db.AccountEntity;
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
|
@ -67,6 +66,7 @@ import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@ -88,8 +88,6 @@ public class NotificationHelper {
|
||||||
|
|
||||||
public static final String REPLY_ACTION = "REPLY_ACTION";
|
public static final String REPLY_ACTION = "REPLY_ACTION";
|
||||||
|
|
||||||
public static final String COMPOSE_ACTION = "COMPOSE_ACTION";
|
|
||||||
|
|
||||||
public static final String KEY_REPLY = "KEY_REPLY";
|
public static final String KEY_REPLY = "KEY_REPLY";
|
||||||
|
|
||||||
public static final String KEY_SENDER_ACCOUNT_ID = "KEY_SENDER_ACCOUNT_ID";
|
public static final String KEY_SENDER_ACCOUNT_ID = "KEY_SENDER_ACCOUNT_ID";
|
||||||
|
@ -108,10 +106,6 @@ public class NotificationHelper {
|
||||||
|
|
||||||
public static final String KEY_MENTIONS = "KEY_MENTIONS";
|
public static final String KEY_MENTIONS = "KEY_MENTIONS";
|
||||||
|
|
||||||
public static final String KEY_CITED_TEXT = "KEY_CITED_TEXT";
|
|
||||||
|
|
||||||
public static final String KEY_CITED_AUTHOR_LOCAL = "KEY_CITED_AUTHOR_LOCAL";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* notification channels used on Android O+
|
* notification channels used on Android O+
|
||||||
**/
|
**/
|
||||||
|
@ -206,21 +200,24 @@ public class NotificationHelper {
|
||||||
.setLabel(context.getString(R.string.label_quick_reply))
|
.setLabel(context.getString(R.string.label_quick_reply))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
PendingIntent quickReplyPendingIntent = getStatusReplyIntent(REPLY_ACTION, context, body, account);
|
PendingIntent quickReplyPendingIntent = getStatusReplyIntent(context, body, account);
|
||||||
|
|
||||||
NotificationCompat.Action quickReplyAction =
|
NotificationCompat.Action quickReplyAction =
|
||||||
new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp,
|
new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp,
|
||||||
context.getString(R.string.action_quick_reply), quickReplyPendingIntent)
|
context.getString(R.string.action_quick_reply),
|
||||||
|
quickReplyPendingIntent)
|
||||||
.addRemoteInput(replyRemoteInput)
|
.addRemoteInput(replyRemoteInput)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
builder.addAction(quickReplyAction);
|
builder.addAction(quickReplyAction);
|
||||||
|
|
||||||
PendingIntent composePendingIntent = getStatusReplyIntent(COMPOSE_ACTION, context, body, account);
|
PendingIntent composeIntent = getStatusComposeIntent(context, body, account);
|
||||||
|
|
||||||
NotificationCompat.Action composeAction =
|
NotificationCompat.Action composeAction =
|
||||||
new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp,
|
new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp,
|
||||||
context.getString(R.string.action_compose_shortcut), composePendingIntent)
|
context.getString(R.string.action_compose_shortcut),
|
||||||
|
composeIntent)
|
||||||
|
.setShowsUserInterface(true)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
builder.addAction(composeAction);
|
builder.addAction(composeAction);
|
||||||
|
@ -237,7 +234,6 @@ public class NotificationHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Summary
|
// Summary
|
||||||
// =======
|
|
||||||
final NotificationCompat.Builder summaryBuilder = newNotification(context, body, account, true);
|
final NotificationCompat.Builder summaryBuilder = newNotification(context, body, account, true);
|
||||||
|
|
||||||
if (currentNotifications.length() != 1) {
|
if (currentNotifications.length() != 1) {
|
||||||
|
@ -275,7 +271,7 @@ public class NotificationHelper {
|
||||||
summaryStackBuilder.addNextIntent(summaryResultIntent);
|
summaryStackBuilder.addNextIntent(summaryResultIntent);
|
||||||
|
|
||||||
PendingIntent summaryResultPendingIntent = summaryStackBuilder.getPendingIntent((int) (notificationId + account.getId() * 10000),
|
PendingIntent summaryResultPendingIntent = summaryStackBuilder.getPendingIntent((int) (notificationId + account.getId() * 10000),
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
pendingIntentFlags(false));
|
||||||
|
|
||||||
// we have to switch account here
|
// we have to switch account here
|
||||||
Intent eventResultIntent = new Intent(context, MainActivity.class);
|
Intent eventResultIntent = new Intent(context, MainActivity.class);
|
||||||
|
@ -285,18 +281,18 @@ public class NotificationHelper {
|
||||||
eventStackBuilder.addNextIntent(eventResultIntent);
|
eventStackBuilder.addNextIntent(eventResultIntent);
|
||||||
|
|
||||||
PendingIntent eventResultPendingIntent = eventStackBuilder.getPendingIntent((int) account.getId(),
|
PendingIntent eventResultPendingIntent = eventStackBuilder.getPendingIntent((int) account.getId(),
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
pendingIntentFlags(false));
|
||||||
|
|
||||||
Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class);
|
Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class);
|
||||||
deleteIntent.putExtra(ACCOUNT_ID, account.getId());
|
deleteIntent.putExtra(ACCOUNT_ID, account.getId());
|
||||||
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, summary ? (int) account.getId() : notificationId, deleteIntent,
|
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, summary ? (int) account.getId() : notificationId, deleteIntent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
pendingIntentFlags(false));
|
||||||
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getChannelId(account, body))
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getChannelId(account, body))
|
||||||
.setSmallIcon(R.drawable.ic_notify)
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
.setContentIntent(summary ? summaryResultPendingIntent : eventResultPendingIntent)
|
.setContentIntent(summary ? summaryResultPendingIntent : eventResultPendingIntent)
|
||||||
.setDeleteIntent(deletePendingIntent)
|
.setDeleteIntent(deletePendingIntent)
|
||||||
.setColor(BuildConfig.FLAVOR == "green" ? Color.parseColor("#19A341") : ContextCompat.getColor(context, R.color.tusky_blue))
|
.setColor(ContextCompat.getColor(context, R.color.notification_color))
|
||||||
.setGroup(account.getAccountId())
|
.setGroup(account.getAccountId())
|
||||||
.setAutoCancel(true)
|
.setAutoCancel(true)
|
||||||
.setShortcutId(Long.toString(account.getId()))
|
.setShortcutId(Long.toString(account.getId()))
|
||||||
|
@ -307,11 +303,9 @@ public class NotificationHelper {
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static PendingIntent getStatusReplyIntent(String action, Context context, Notification body, AccountEntity account) {
|
private static PendingIntent getStatusReplyIntent(Context context, Notification body, AccountEntity account) {
|
||||||
Status status = body.getStatus();
|
Status status = body.getStatus();
|
||||||
|
|
||||||
String citedLocalAuthor = status.getAccount().getLocalUsername();
|
|
||||||
String citedText = status.getContent().toString();
|
|
||||||
String inReplyToId = status.getId();
|
String inReplyToId = status.getId();
|
||||||
Status actionableStatus = status.getActionableStatus();
|
Status actionableStatus = status.getActionableStatus();
|
||||||
Status.Visibility replyVisibility = actionableStatus.getVisibility();
|
Status.Visibility replyVisibility = actionableStatus.getVisibility();
|
||||||
|
@ -326,9 +320,7 @@ public class NotificationHelper {
|
||||||
mentionedUsernames = new ArrayList<>(new LinkedHashSet<>(mentionedUsernames));
|
mentionedUsernames = new ArrayList<>(new LinkedHashSet<>(mentionedUsernames));
|
||||||
|
|
||||||
Intent replyIntent = new Intent(context, SendStatusBroadcastReceiver.class)
|
Intent replyIntent = new Intent(context, SendStatusBroadcastReceiver.class)
|
||||||
.setAction(action)
|
.setAction(REPLY_ACTION)
|
||||||
.putExtra(KEY_CITED_AUTHOR_LOCAL, citedLocalAuthor)
|
|
||||||
.putExtra(KEY_CITED_TEXT, citedText)
|
|
||||||
.putExtra(KEY_SENDER_ACCOUNT_ID, account.getId())
|
.putExtra(KEY_SENDER_ACCOUNT_ID, account.getId())
|
||||||
.putExtra(KEY_SENDER_ACCOUNT_IDENTIFIER, account.getIdentifier())
|
.putExtra(KEY_SENDER_ACCOUNT_IDENTIFIER, account.getIdentifier())
|
||||||
.putExtra(KEY_SENDER_ACCOUNT_FULL_NAME, account.getFullName())
|
.putExtra(KEY_SENDER_ACCOUNT_FULL_NAME, account.getFullName())
|
||||||
|
@ -341,7 +333,50 @@ public class NotificationHelper {
|
||||||
return PendingIntent.getBroadcast(context.getApplicationContext(),
|
return PendingIntent.getBroadcast(context.getApplicationContext(),
|
||||||
notificationId,
|
notificationId,
|
||||||
replyIntent,
|
replyIntent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
pendingIntentFlags(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PendingIntent getStatusComposeIntent(Context context, Notification body, AccountEntity account) {
|
||||||
|
Status status = body.getStatus();
|
||||||
|
|
||||||
|
String citedLocalAuthor = status.getAccount().getLocalUsername();
|
||||||
|
String citedText = status.getContent().toString();
|
||||||
|
String inReplyToId = status.getId();
|
||||||
|
Status actionableStatus = status.getActionableStatus();
|
||||||
|
Status.Visibility replyVisibility = actionableStatus.getVisibility();
|
||||||
|
String contentWarning = actionableStatus.getSpoilerText();
|
||||||
|
List<Status.Mention> mentions = actionableStatus.getMentions();
|
||||||
|
Set<String> mentionedUsernames = new LinkedHashSet<>();
|
||||||
|
mentionedUsernames.add(actionableStatus.getAccount().getUsername());
|
||||||
|
for (Status.Mention mention : mentions) {
|
||||||
|
String mentionedUsername = mention.getUsername();
|
||||||
|
if (!mentionedUsername.equals(account.getUsername())) {
|
||||||
|
mentionedUsernames.add(mention.getUsername());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ComposeActivity.ComposeOptions composeOptions = new ComposeActivity.ComposeOptions();
|
||||||
|
composeOptions.setInReplyToId(inReplyToId);
|
||||||
|
composeOptions.setReplyVisibility(replyVisibility);
|
||||||
|
composeOptions.setContentWarning(contentWarning);
|
||||||
|
composeOptions.setReplyingStatusAuthor(citedLocalAuthor);
|
||||||
|
composeOptions.setReplyingStatusContent(citedText);
|
||||||
|
composeOptions.setMentionedUsernames(mentionedUsernames);
|
||||||
|
composeOptions.setModifiedInitialState(true);
|
||||||
|
|
||||||
|
Intent composeIntent = ComposeActivity.startIntent(
|
||||||
|
context,
|
||||||
|
composeOptions,
|
||||||
|
notificationId,
|
||||||
|
account.getId()
|
||||||
|
);
|
||||||
|
|
||||||
|
composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
|
||||||
|
return PendingIntent.getActivity(context.getApplicationContext(),
|
||||||
|
notificationId,
|
||||||
|
composeIntent,
|
||||||
|
pendingIntentFlags(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void createNotificationChannelsForAccount(@NonNull AccountEntity account, @NonNull Context context) {
|
public static void createNotificationChannelsForAccount(@NonNull AccountEntity account, @NonNull Context context) {
|
||||||
|
@ -409,9 +444,7 @@ public class NotificationHelper {
|
||||||
|
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
notificationManager.deleteNotificationChannelGroup(account.getIdentifier());
|
notificationManager.deleteNotificationChannelGroup(account.getIdentifier());
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -421,7 +454,6 @@ public class NotificationHelper {
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
// used until Tusky 1.4
|
// used until Tusky 1.4
|
||||||
//noinspection ConstantConditions
|
|
||||||
notificationManager.deleteNotificationChannel(CHANNEL_MENTION);
|
notificationManager.deleteNotificationChannel(CHANNEL_MENTION);
|
||||||
notificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE);
|
notificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE);
|
||||||
notificationManager.deleteNotificationChannel(CHANNEL_BOOST);
|
notificationManager.deleteNotificationChannel(CHANNEL_BOOST);
|
||||||
|
@ -440,7 +472,6 @@ public class NotificationHelper {
|
||||||
// on Android >= O, notifications are enabled, if at least one channel is enabled
|
// on Android >= O, notifications are enabled, if at least one channel is enabled
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
//noinspection ConstantConditions
|
|
||||||
if (notificationManager.areNotificationsEnabled()) {
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
for (NotificationChannel channel : notificationManager.getNotificationChannels()) {
|
for (NotificationChannel channel : notificationManager.getNotificationChannels()) {
|
||||||
if (channel.getImportance() > NotificationManager.IMPORTANCE_NONE) {
|
if (channel.getImportance() > NotificationManager.IMPORTANCE_NONE) {
|
||||||
|
@ -491,7 +522,6 @@ public class NotificationHelper {
|
||||||
accountManager.saveAccount(account);
|
accountManager.saveAccount(account);
|
||||||
|
|
||||||
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
//noinspection ConstantConditions
|
|
||||||
notificationManager.cancel((int) account.getId());
|
notificationManager.cancel((int) account.getId());
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
|
@ -511,7 +541,6 @@ public class NotificationHelper {
|
||||||
// unknown notificationtype
|
// unknown notificationtype
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
//noinspection ConstantConditions
|
|
||||||
NotificationChannel channel = notificationManager.getNotificationChannel(channelId);
|
NotificationChannel channel = notificationManager.getNotificationChannel(channelId);
|
||||||
return channel.getImportance() > NotificationManager.IMPORTANCE_NONE;
|
return channel.getImportance() > NotificationManager.IMPORTANCE_NONE;
|
||||||
}
|
}
|
||||||
|
@ -674,4 +703,11 @@ public class NotificationHelper {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int pendingIntentFlags(boolean mutable) {
|
||||||
|
if (mutable) {
|
||||||
|
return PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0);
|
||||||
|
} else {
|
||||||
|
return PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PendingIntent.FLAG_IMMUTABLE : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,8 +13,9 @@ import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.keylesspalace.tusky.MainActivity
|
||||||
import com.keylesspalace.tusky.R
|
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.DialogEmojicompatBinding
|
||||||
import com.keylesspalace.tusky.databinding.ItemEmojiPrefBinding
|
import com.keylesspalace.tusky.databinding.ItemEmojiPrefBinding
|
||||||
import com.keylesspalace.tusky.util.EmojiCompatFont
|
import com.keylesspalace.tusky.util.EmojiCompatFont
|
||||||
|
@ -215,12 +216,12 @@ class EmojiPreference(
|
||||||
.setPositiveButton(R.string.restart) { _, _ ->
|
.setPositiveButton(R.string.restart) { _, _ ->
|
||||||
// Restart the app
|
// Restart the app
|
||||||
// From https://stackoverflow.com/a/17166729/5070653
|
// From https://stackoverflow.com/a/17166729/5070653
|
||||||
val launchIntent = Intent(context, SplashActivity::class.java)
|
val launchIntent = Intent(context, MainActivity::class.java)
|
||||||
val mPendingIntent = PendingIntent.getActivity(
|
val mPendingIntent = PendingIntent.getActivity(
|
||||||
context,
|
context,
|
||||||
0x1f973, // This is the codepoint of the party face emoji :D
|
0x1f973, // This is the codepoint of the party face emoji :D
|
||||||
launchIntent,
|
launchIntent,
|
||||||
PendingIntent.FLAG_CANCEL_CURRENT
|
NotificationHelper.pendingIntentFlags(false)
|
||||||
)
|
)
|
||||||
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
||||||
mgr.set(
|
mgr.set(
|
||||||
|
|
|
@ -78,7 +78,7 @@ class PreferencesActivity :
|
||||||
NotificationPreferencesFragment.newInstance()
|
NotificationPreferencesFragment.newInstance()
|
||||||
}
|
}
|
||||||
TAB_FILTER_PREFERENCES -> {
|
TAB_FILTER_PREFERENCES -> {
|
||||||
setTitle(R.string.pref_title_status_tabs)
|
setTitle(R.string.pref_title_post_tabs)
|
||||||
TabFilterPreferencesFragment.newInstance()
|
TabFilterPreferencesFragment.newInstance()
|
||||||
}
|
}
|
||||||
PROXY_PREFERENCES -> {
|
PROXY_PREFERENCES -> {
|
||||||
|
|
|
@ -25,7 +25,14 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.entity.Notification
|
import com.keylesspalace.tusky.entity.Notification
|
||||||
import com.keylesspalace.tusky.settings.*
|
import com.keylesspalace.tusky.settings.AppTheme
|
||||||
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
|
import com.keylesspalace.tusky.settings.emojiPreference
|
||||||
|
import com.keylesspalace.tusky.settings.listPreference
|
||||||
|
import com.keylesspalace.tusky.settings.makePreferenceScreen
|
||||||
|
import com.keylesspalace.tusky.settings.preference
|
||||||
|
import com.keylesspalace.tusky.settings.preferenceCategory
|
||||||
|
import com.keylesspalace.tusky.settings.switchPreference
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import com.keylesspalace.tusky.util.deserialize
|
import com.keylesspalace.tusky.util.deserialize
|
||||||
import com.keylesspalace.tusky.util.getNonNullString
|
import com.keylesspalace.tusky.util.getNonNullString
|
||||||
|
@ -85,11 +92,11 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
|
|
||||||
listPreference {
|
listPreference {
|
||||||
setDefaultValue("medium")
|
setDefaultValue("medium")
|
||||||
setEntries(R.array.status_text_size_names)
|
setEntries(R.array.post_text_size_names)
|
||||||
setEntryValues(R.array.status_text_size_values)
|
setEntryValues(R.array.post_text_size_values)
|
||||||
key = PrefKeys.STATUS_TEXT_SIZE
|
key = PrefKeys.STATUS_TEXT_SIZE
|
||||||
setSummaryProvider { entry }
|
setSummaryProvider { entry }
|
||||||
setTitle(R.string.pref_status_text_size)
|
setTitle(R.string.pref_post_text_size)
|
||||||
icon = makeIcon(GoogleMaterial.Icon.gmd_format_size)
|
icon = makeIcon(GoogleMaterial.Icon.gmd_format_size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +144,13 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
isSingleLineTitle = false
|
isSingleLineTitle = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switchPreference {
|
||||||
|
setDefaultValue(false)
|
||||||
|
key = PrefKeys.ANIMATE_CUSTOM_EMOJIS
|
||||||
|
setTitle(R.string.pref_title_animate_custom_emojis)
|
||||||
|
isSingleLineTitle = false
|
||||||
|
}
|
||||||
|
|
||||||
switchPreference {
|
switchPreference {
|
||||||
setDefaultValue(true)
|
setDefaultValue(true)
|
||||||
key = PrefKeys.USE_BLURHASH
|
key = PrefKeys.USE_BLURHASH
|
||||||
|
@ -179,13 +193,6 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
isSingleLineTitle = false
|
isSingleLineTitle = false
|
||||||
}
|
}
|
||||||
|
|
||||||
switchPreference {
|
|
||||||
setDefaultValue(false)
|
|
||||||
key = PrefKeys.ANIMATE_CUSTOM_EMOJIS
|
|
||||||
setTitle(R.string.pref_title_animate_custom_emojis)
|
|
||||||
isSingleLineTitle = false
|
|
||||||
}
|
|
||||||
|
|
||||||
switchPreference {
|
switchPreference {
|
||||||
setDefaultValue(true)
|
setDefaultValue(true)
|
||||||
key = PrefKeys.USE_QUICK_TOOT
|
key = PrefKeys.USE_QUICK_TOOT
|
||||||
|
@ -232,7 +239,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
|
|
||||||
preferenceCategory(R.string.pref_title_timeline_filters) {
|
preferenceCategory(R.string.pref_title_timeline_filters) {
|
||||||
preference {
|
preference {
|
||||||
setTitle(R.string.pref_title_status_tabs)
|
setTitle(R.string.pref_title_post_tabs)
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.TAB_FILTER_PREFERENCES)
|
val intent = PreferencesActivity.newIntent(activity, PreferencesActivity.TAB_FILTER_PREFERENCES)
|
||||||
|
@ -305,7 +312,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
preferenceManager.sharedPreferences.let { prefs ->
|
preferenceManager.sharedPreferences?.let { prefs ->
|
||||||
prefs.getString(PrefKeys.STACK_TRACE, null)?.let { stackTrace ->
|
prefs.getString(PrefKeys.STACK_TRACE, null)?.let { stackTrace ->
|
||||||
preferenceCategory(R.string.pref_title_stacktrace) {
|
preferenceCategory(R.string.pref_title_stacktrace) {
|
||||||
preference {
|
preference {
|
||||||
|
@ -313,7 +320,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
setOnPreferenceClickListener {
|
setOnPreferenceClickListener {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val intent = ComposeActivity.startIntent(activity, ComposeOptions(
|
val intent = ComposeActivity.startIntent(activity, ComposeOptions(
|
||||||
tootText = "@ars42525@odakyu.app $stackTrace".substring(0, 400),
|
content = "@ars42525@odakyu.app $stackTrace".substring(0, 400),
|
||||||
contentWarning = "Yuito StackTrace"
|
contentWarning = "Yuito StackTrace"
|
||||||
))
|
))
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
|
@ -350,23 +357,24 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateHttpProxySummary() {
|
private fun updateHttpProxySummary() {
|
||||||
val sharedPreferences = preferenceManager.sharedPreferences
|
preferenceManager.sharedPreferences?.let { sharedPreferences ->
|
||||||
val httpProxyEnabled = sharedPreferences.getBoolean(PrefKeys.HTTP_PROXY_ENABLED, false)
|
val httpProxyEnabled = sharedPreferences.getBoolean(PrefKeys.HTTP_PROXY_ENABLED, false)
|
||||||
val httpServer = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_SERVER, "")
|
val httpServer = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_SERVER, "")
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val httpPort = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1")
|
val httpPort = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1")
|
||||||
.toInt()
|
.toInt()
|
||||||
|
|
||||||
if (httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) {
|
if (httpProxyEnabled && httpServer.isNotBlank() && httpPort > 0 && httpPort < 65535) {
|
||||||
httpProxyPref?.summary = "$httpServer:$httpPort"
|
httpProxyPref?.summary = "$httpServer:$httpPort"
|
||||||
return
|
return
|
||||||
|
}
|
||||||
|
} catch (e: NumberFormatException) {
|
||||||
|
// user has entered wrong port, fall back to empty summary
|
||||||
}
|
}
|
||||||
} catch (e: NumberFormatException) {
|
|
||||||
// user has entered wrong port, fall back to empty summary
|
|
||||||
}
|
|
||||||
|
|
||||||
httpProxyPref?.summary = ""
|
httpProxyPref?.summary = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -123,9 +123,9 @@ class StatusViewHolder(
|
||||||
|
|
||||||
private fun setContentWarningButtonText(contentShown: Boolean) {
|
private fun setContentWarningButtonText(contentShown: Boolean) {
|
||||||
if (contentShown) {
|
if (contentShown) {
|
||||||
binding.statusContentWarningButton.setText(R.string.status_content_warning_show_less)
|
binding.statusContentWarningButton.setText(R.string.post_content_warning_show_less)
|
||||||
} else {
|
} else {
|
||||||
binding.statusContentWarningButton.setText(R.string.status_content_warning_show_more)
|
binding.statusContentWarningButton.setText(R.string.post_content_warning_show_more)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,10 +178,10 @@ class StatusViewHolder(
|
||||||
|
|
||||||
binding.buttonToggleContent.show()
|
binding.buttonToggleContent.show()
|
||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
binding.buttonToggleContent.setText(R.string.status_content_show_more)
|
binding.buttonToggleContent.setText(R.string.post_content_show_more)
|
||||||
binding.statusContent.filters = COLLAPSE_INPUT_FILTER
|
binding.statusContent.filters = COLLAPSE_INPUT_FILTER
|
||||||
} else {
|
} else {
|
||||||
binding.buttonToggleContent.setText(R.string.status_content_show_less)
|
binding.buttonToggleContent.setText(R.string.post_content_show_less)
|
||||||
binding.statusContent.filters = NO_INPUT_FILTER
|
binding.statusContent.filters = NO_INPUT_FILTER
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -154,7 +154,7 @@ class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Inje
|
||||||
|
|
||||||
private fun showError() {
|
private fun showError() {
|
||||||
if (snackbarErrorRetry?.isShown != true) {
|
if (snackbarErrorRetry?.isShown != true) {
|
||||||
snackbarErrorRetry = Snackbar.make(binding.swipeRefreshLayout, R.string.failed_fetch_statuses, Snackbar.LENGTH_INDEFINITE)
|
snackbarErrorRetry = Snackbar.make(binding.swipeRefreshLayout, R.string.failed_fetch_posts, Snackbar.LENGTH_INDEFINITE)
|
||||||
snackbarErrorRetry?.setAction(R.string.action_retry) {
|
snackbarErrorRetry?.setAction(R.string.action_retry) {
|
||||||
adapter.retry()
|
adapter.retry()
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.databinding.ActivityScheduledTootBinding
|
import com.keylesspalace.tusky.databinding.ActivityScheduledStatusBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||||
|
@ -40,7 +40,7 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injectable {
|
class ScheduledStatusActivity : BaseActivity(), ScheduledStatusActionListener, Injectable {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var viewModelFactory: ViewModelFactory
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
@ -48,19 +48,19 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var eventHub: EventHub
|
lateinit var eventHub: EventHub
|
||||||
|
|
||||||
private val viewModel: ScheduledTootViewModel by viewModels { viewModelFactory }
|
private val viewModel: ScheduledStatusViewModel by viewModels { viewModelFactory }
|
||||||
|
|
||||||
private val adapter = ScheduledTootAdapter(this)
|
private val adapter = ScheduledStatusAdapter(this)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
val binding = ActivityScheduledTootBinding.inflate(layoutInflater)
|
val binding = ActivityScheduledStatusBinding.inflate(layoutInflater)
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||||
supportActionBar?.run {
|
supportActionBar?.run {
|
||||||
title = getString(R.string.title_scheduled_toot)
|
title = getString(R.string.title_scheduled_posts)
|
||||||
setDisplayHomeAsUpEnabled(true)
|
setDisplayHomeAsUpEnabled(true)
|
||||||
setDisplayShowHomeEnabled(true)
|
setDisplayShowHomeEnabled(true)
|
||||||
}
|
}
|
||||||
|
@ -94,7 +94,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
||||||
if (loadState.refresh is LoadState.NotLoading) {
|
if (loadState.refresh is LoadState.NotLoading) {
|
||||||
binding.progressBar.hide()
|
binding.progressBar.hide()
|
||||||
if (adapter.itemCount == 0) {
|
if (adapter.itemCount == 0) {
|
||||||
binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_status)
|
binding.errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_scheduled_posts)
|
||||||
binding.errorMessageView.show()
|
binding.errorMessageView.show()
|
||||||
} else {
|
} else {
|
||||||
binding.errorMessageView.hide()
|
binding.errorMessageView.hide()
|
||||||
|
@ -121,7 +121,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
||||||
this,
|
this,
|
||||||
ComposeActivity.ComposeOptions(
|
ComposeActivity.ComposeOptions(
|
||||||
scheduledTootId = item.id,
|
scheduledTootId = item.id,
|
||||||
tootText = item.params.text,
|
content = item.params.text,
|
||||||
contentWarning = item.params.spoilerText,
|
contentWarning = item.params.spoilerText,
|
||||||
mediaAttachments = item.mediaAttachments,
|
mediaAttachments = item.mediaAttachments,
|
||||||
inReplyToId = item.params.inReplyToId,
|
inReplyToId = item.params.inReplyToId,
|
||||||
|
@ -138,6 +138,6 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newIntent(context: Context) = Intent(context, ScheduledTootActivity::class.java)
|
fun newIntent(context: Context) = Intent(context, ScheduledStatusActivity::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -20,18 +20,18 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.paging.PagingDataAdapter
|
import androidx.paging.PagingDataAdapter
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import com.keylesspalace.tusky.databinding.ItemScheduledTootBinding
|
import com.keylesspalace.tusky.databinding.ItemScheduledStatusBinding
|
||||||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
|
|
||||||
interface ScheduledTootActionListener {
|
interface ScheduledStatusActionListener {
|
||||||
fun edit(item: ScheduledStatus)
|
fun edit(item: ScheduledStatus)
|
||||||
fun delete(item: ScheduledStatus)
|
fun delete(item: ScheduledStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScheduledTootAdapter(
|
class ScheduledStatusAdapter(
|
||||||
val listener: ScheduledTootActionListener
|
val listener: ScheduledStatusActionListener
|
||||||
) : PagingDataAdapter<ScheduledStatus, BindingHolder<ItemScheduledTootBinding>>(
|
) : PagingDataAdapter<ScheduledStatus, BindingHolder<ItemScheduledStatusBinding>>(
|
||||||
object : DiffUtil.ItemCallback<ScheduledStatus>() {
|
object : DiffUtil.ItemCallback<ScheduledStatus>() {
|
||||||
override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean {
|
override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean {
|
||||||
return oldItem.id == newItem.id
|
return oldItem.id == newItem.id
|
||||||
|
@ -43,12 +43,12 @@ class ScheduledTootAdapter(
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemScheduledTootBinding> {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemScheduledStatusBinding> {
|
||||||
val binding = ItemScheduledTootBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
val binding = ItemScheduledStatusBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
return BindingHolder(binding)
|
return BindingHolder(binding)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BindingHolder<ItemScheduledTootBinding>, position: Int) {
|
override fun onBindViewHolder(holder: BindingHolder<ItemScheduledStatusBinding>, position: Int) {
|
||||||
getItem(position)?.let { item ->
|
getItem(position)?.let { item ->
|
||||||
holder.binding.edit.isEnabled = true
|
holder.binding.edit.isEnabled = true
|
||||||
holder.binding.delete.isEnabled = true
|
holder.binding.delete.isEnabled = true
|
|
@ -22,16 +22,16 @@ import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
|
|
||||||
class ScheduledTootPagingSourceFactory(
|
class ScheduledStatusPagingSourceFactory(
|
||||||
private val mastodonApi: MastodonApi
|
private val mastodonApi: MastodonApi
|
||||||
) : () -> ScheduledTootPagingSource {
|
) : () -> ScheduledStatusPagingSource {
|
||||||
|
|
||||||
private val scheduledTootsCache = mutableListOf<ScheduledStatus>()
|
private val scheduledTootsCache = mutableListOf<ScheduledStatus>()
|
||||||
|
|
||||||
private var pagingSource: ScheduledTootPagingSource? = null
|
private var pagingSource: ScheduledStatusPagingSource? = null
|
||||||
|
|
||||||
override fun invoke(): ScheduledTootPagingSource {
|
override fun invoke(): ScheduledStatusPagingSource {
|
||||||
return ScheduledTootPagingSource(mastodonApi, scheduledTootsCache).also {
|
return ScheduledStatusPagingSource(mastodonApi, scheduledTootsCache).also {
|
||||||
pagingSource = it
|
pagingSource = it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,9 +42,9 @@ class ScheduledTootPagingSourceFactory(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ScheduledTootPagingSource(
|
class ScheduledStatusPagingSource(
|
||||||
private val mastodonApi: MastodonApi,
|
private val mastodonApi: MastodonApi,
|
||||||
private val scheduledTootsCache: MutableList<ScheduledStatus>
|
private val scheduledStatusesCache: MutableList<ScheduledStatus>
|
||||||
) : PagingSource<String, ScheduledStatus>() {
|
) : PagingSource<String, ScheduledStatus>() {
|
||||||
|
|
||||||
override fun getRefreshKey(state: PagingState<String, ScheduledStatus>): String? {
|
override fun getRefreshKey(state: PagingState<String, ScheduledStatus>): String? {
|
||||||
|
@ -52,11 +52,11 @@ class ScheduledTootPagingSource(
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun load(params: LoadParams<String>): LoadResult<String, ScheduledStatus> {
|
override suspend fun load(params: LoadParams<String>): LoadResult<String, ScheduledStatus> {
|
||||||
return if (params is LoadParams.Refresh && scheduledTootsCache.isNotEmpty()) {
|
return if (params is LoadParams.Refresh && scheduledStatusesCache.isNotEmpty()) {
|
||||||
LoadResult.Page(
|
LoadResult.Page(
|
||||||
data = scheduledTootsCache,
|
data = scheduledStatusesCache,
|
||||||
prevKey = null,
|
prevKey = null,
|
||||||
nextKey = scheduledTootsCache.lastOrNull()?.id
|
nextKey = scheduledStatusesCache.lastOrNull()?.id
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
@ -71,7 +71,7 @@ class ScheduledTootPagingSource(
|
||||||
nextKey = result.lastOrNull()?.id
|
nextKey = result.lastOrNull()?.id
|
||||||
)
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w("ScheduledTootPgngSrc", "Error loading scheduled statuses", e)
|
Log.w("ScheduledStatuses", "Error loading scheduled statuses", e)
|
||||||
LoadResult.Error(e)
|
LoadResult.Error(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -28,12 +28,12 @@ import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ScheduledTootViewModel @Inject constructor(
|
class ScheduledStatusViewModel @Inject constructor(
|
||||||
val mastodonApi: MastodonApi,
|
val mastodonApi: MastodonApi,
|
||||||
val eventHub: EventHub
|
val eventHub: EventHub
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val pagingSourceFactory = ScheduledTootPagingSourceFactory(mastodonApi)
|
private val pagingSourceFactory = ScheduledStatusPagingSourceFactory(mastodonApi)
|
||||||
|
|
||||||
val data = Pager(
|
val data = Pager(
|
||||||
config = PagingConfig(pageSize = 20, initialLoadSize = 20),
|
config = PagingConfig(pageSize = 20, initialLoadSize = 20),
|
|
@ -87,7 +87,7 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
|
|
||||||
private fun getPageTitle(position: Int): CharSequence {
|
private fun getPageTitle(position: Int): CharSequence {
|
||||||
return when (position) {
|
return when (position) {
|
||||||
0 -> getString(R.string.title_statuses)
|
0 -> getString(R.string.title_posts)
|
||||||
1 -> getString(R.string.title_accounts)
|
1 -> getString(R.string.title_accounts)
|
||||||
2 -> getString(R.string.title_hashtags_dialog)
|
2 -> getString(R.string.title_hashtags_dialog)
|
||||||
3 -> getString(R.string.title_notestock)
|
3 -> getString(R.string.title_notestock)
|
||||||
|
|
|
@ -21,11 +21,11 @@ import androidx.paging.PagingDataAdapter
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.adapter.AccountViewHolder
|
import com.keylesspalace.tusky.adapter.AccountViewHolder
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
|
|
||||||
class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) :
|
class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) :
|
||||||
PagingDataAdapter<Account, AccountViewHolder>(ACCOUNT_COMPARATOR) {
|
PagingDataAdapter<TimelineAccount, AccountViewHolder>(ACCOUNT_COMPARATOR) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder {
|
||||||
val view = LayoutInflater.from(parent.context)
|
val view = LayoutInflater.from(parent.context)
|
||||||
|
@ -44,11 +44,11 @@ class SearchAccountsAdapter(private val linkListener: LinkListener, private val
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback<Account>() {
|
val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback<TimelineAccount>() {
|
||||||
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean =
|
override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean =
|
||||||
oldItem.deepEquals(newItem)
|
oldItem == newItem
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean =
|
override fun areItemsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean =
|
||||||
oldItem.id == newItem.id
|
oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,12 @@ import androidx.paging.PagingData
|
||||||
import androidx.paging.PagingDataAdapter
|
import androidx.paging.PagingDataAdapter
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter
|
import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class SearchAccountsFragment : SearchFragment<Account>() {
|
class SearchAccountsFragment : SearchFragment<TimelineAccount>() {
|
||||||
override fun createAdapter(): PagingDataAdapter<Account, *> {
|
override fun createAdapter(): PagingDataAdapter<TimelineAccount, *> {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(binding.searchRecyclerView.context)
|
||||||
|
|
||||||
return SearchAccountsAdapter(
|
return SearchAccountsAdapter(
|
||||||
|
@ -34,7 +34,7 @@ class SearchAccountsFragment : SearchFragment<Account>() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override val data: Flow<PagingData<Account>>
|
override val data: Flow<PagingData<TimelineAccount>>
|
||||||
get() = viewModel.accountsFlow
|
get() = viewModel.accountsFlow
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -300,7 +300,7 @@ class SearchNotestockFragment : SearchFragment<StatusViewData.Concrete>(), Statu
|
||||||
|
|
||||||
popup.setOnMenuItemClickListener { item ->
|
popup.setOnMenuItemClickListener { item ->
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.status_share_content -> {
|
R.id.post_share_content -> {
|
||||||
val statusToShare: Status = status.actionableStatus
|
val statusToShare: Status = status.actionableStatus
|
||||||
|
|
||||||
val sendIntent = Intent()
|
val sendIntent = Intent()
|
||||||
|
@ -314,12 +314,12 @@ class SearchNotestockFragment : SearchFragment<StatusViewData.Concrete>(), Statu
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent.createChooser(
|
Intent.createChooser(
|
||||||
sendIntent,
|
sendIntent,
|
||||||
resources.getText(R.string.send_status_content_to)
|
resources.getText(R.string.send_post_content_to)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return@setOnMenuItemClickListener true
|
return@setOnMenuItemClickListener true
|
||||||
}
|
}
|
||||||
R.id.status_share_link -> {
|
R.id.post_share_link -> {
|
||||||
val sendIntent = Intent()
|
val sendIntent = Intent()
|
||||||
sendIntent.action = Intent.ACTION_SEND
|
sendIntent.action = Intent.ACTION_SEND
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl)
|
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl)
|
||||||
|
@ -327,7 +327,7 @@ class SearchNotestockFragment : SearchFragment<StatusViewData.Concrete>(), Statu
|
||||||
startActivity(
|
startActivity(
|
||||||
Intent.createChooser(
|
Intent.createChooser(
|
||||||
sendIntent,
|
sendIntent,
|
||||||
resources.getText(R.string.send_status_link_to)
|
resources.getText(R.string.send_post_link_to)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return@setOnMenuItemClickListener true
|
return@setOnMenuItemClickListener true
|
||||||
|
@ -456,7 +456,7 @@ class SearchNotestockFragment : SearchFragment<StatusViewData.Concrete>(), Statu
|
||||||
private fun showConfirmDeleteDialog(id: String, position: Int) {
|
private fun showConfirmDeleteDialog(id: String, position: Int) {
|
||||||
context?.let {
|
context?.let {
|
||||||
AlertDialog.Builder(it)
|
AlertDialog.Builder(it)
|
||||||
.setMessage(R.string.dialog_delete_toot_warning)
|
.setMessage(R.string.dialog_delete_post_warning)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
viewModel.deleteStatus(id)
|
viewModel.deleteStatus(id)
|
||||||
removeItem(position)
|
removeItem(position)
|
||||||
|
@ -469,7 +469,7 @@ class SearchNotestockFragment : SearchFragment<StatusViewData.Concrete>(), Statu
|
||||||
private fun showConfirmEditDialog(id: String, position: Int, status: Status) {
|
private fun showConfirmEditDialog(id: String, position: Int, status: Status) {
|
||||||
activity?.let {
|
activity?.let {
|
||||||
AlertDialog.Builder(it)
|
AlertDialog.Builder(it)
|
||||||
.setMessage(R.string.dialog_redraft_toot_warning)
|
.setMessage(R.string.dialog_redraft_post_warning)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
viewModel.deleteStatus(id)
|
viewModel.deleteStatus(id)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -486,7 +486,7 @@ class SearchNotestockFragment : SearchFragment<StatusViewData.Concrete>(), Statu
|
||||||
val intent = ComposeActivity.startIntent(
|
val intent = ComposeActivity.startIntent(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
ComposeOptions(
|
ComposeOptions(
|
||||||
tootText = redraftStatus.text ?: "",
|
content = redraftStatus.text ?: "",
|
||||||
inReplyToId = redraftStatus.inReplyToId,
|
inReplyToId = redraftStatus.inReplyToId,
|
||||||
visibility = redraftStatus.visibility,
|
visibility = redraftStatus.visibility,
|
||||||
contentWarning = redraftStatus.spoilerText,
|
contentWarning = redraftStatus.spoilerText,
|
||||||
|
|
|
@ -310,7 +310,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
|
|
||||||
popup.setOnMenuItemClickListener { item ->
|
popup.setOnMenuItemClickListener { item ->
|
||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
R.id.status_share_content -> {
|
R.id.post_share_content -> {
|
||||||
val statusToShare: Status = status.actionableStatus
|
val statusToShare: Status = status.actionableStatus
|
||||||
|
|
||||||
val sendIntent = Intent()
|
val sendIntent = Intent()
|
||||||
|
@ -321,15 +321,15 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
statusToShare.content.toString()
|
statusToShare.content.toString()
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare)
|
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare)
|
||||||
sendIntent.type = "text/plain"
|
sendIntent.type = "text/plain"
|
||||||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_status_content_to)))
|
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_post_content_to)))
|
||||||
return@setOnMenuItemClickListener true
|
return@setOnMenuItemClickListener true
|
||||||
}
|
}
|
||||||
R.id.status_share_link -> {
|
R.id.post_share_link -> {
|
||||||
val sendIntent = Intent()
|
val sendIntent = Intent()
|
||||||
sendIntent.action = Intent.ACTION_SEND
|
sendIntent.action = Intent.ACTION_SEND
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl)
|
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl)
|
||||||
sendIntent.type = "text/plain"
|
sendIntent.type = "text/plain"
|
||||||
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_status_link_to)))
|
startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_post_link_to)))
|
||||||
return@setOnMenuItemClickListener true
|
return@setOnMenuItemClickListener true
|
||||||
}
|
}
|
||||||
R.id.status_copy_link -> {
|
R.id.status_copy_link -> {
|
||||||
|
@ -454,7 +454,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
private fun showConfirmDeleteDialog(id: String, position: Int) {
|
private fun showConfirmDeleteDialog(id: String, position: Int) {
|
||||||
context?.let {
|
context?.let {
|
||||||
AlertDialog.Builder(it)
|
AlertDialog.Builder(it)
|
||||||
.setMessage(R.string.dialog_delete_toot_warning)
|
.setMessage(R.string.dialog_delete_post_warning)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
viewModel.deleteStatus(id)
|
viewModel.deleteStatus(id)
|
||||||
removeItem(position)
|
removeItem(position)
|
||||||
|
@ -467,7 +467,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
private fun showConfirmEditDialog(id: String, position: Int, status: Status) {
|
private fun showConfirmEditDialog(id: String, position: Int, status: Status) {
|
||||||
activity?.let {
|
activity?.let {
|
||||||
AlertDialog.Builder(it)
|
AlertDialog.Builder(it)
|
||||||
.setMessage(R.string.dialog_redraft_toot_warning)
|
.setMessage(R.string.dialog_redraft_post_warning)
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
viewModel.deleteStatus(id)
|
viewModel.deleteStatus(id)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -485,7 +485,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
val intent = ComposeActivity.startIntent(
|
val intent = ComposeActivity.startIntent(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
ComposeOptions(
|
ComposeOptions(
|
||||||
tootText = redraftStatus.text ?: "",
|
content = redraftStatus.text ?: "",
|
||||||
inReplyToId = redraftStatus.inReplyToId,
|
inReplyToId = redraftStatus.inReplyToId,
|
||||||
visibility = redraftStatus.visibility,
|
visibility = redraftStatus.visibility,
|
||||||
contentWarning = redraftStatus.spoilerText,
|
contentWarning = redraftStatus.spoilerText,
|
||||||
|
|
|
@ -90,9 +90,9 @@ class TimelineFragment :
|
||||||
|
|
||||||
private val viewModel: TimelineViewModel by lazy {
|
private val viewModel: TimelineViewModel by lazy {
|
||||||
if (kind == TimelineViewModel.Kind.HOME) {
|
if (kind == TimelineViewModel.Kind.HOME) {
|
||||||
ViewModelProvider(this, viewModelFactory).get(CachedTimelineViewModel::class.java)
|
ViewModelProvider(this, viewModelFactory)[CachedTimelineViewModel::class.java]
|
||||||
} else {
|
} else {
|
||||||
ViewModelProvider(this, viewModelFactory).get(NetworkTimelineViewModel::class.java)
|
ViewModelProvider(this, viewModelFactory)[NetworkTimelineViewModel::class.java]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ class TimelineFragment :
|
||||||
|
|
||||||
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
|
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true)
|
||||||
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
val statusDisplayOptions = StatusDisplayOptions(
|
val statusDisplayOptions = StatusDisplayOptions(
|
||||||
animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false),
|
||||||
mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled,
|
mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled,
|
||||||
|
@ -187,7 +187,7 @@ class TimelineFragment :
|
||||||
if (adapter.itemCount == 0) {
|
if (adapter.itemCount == 0) {
|
||||||
when (loadState.refresh) {
|
when (loadState.refresh) {
|
||||||
is LoadState.NotLoading -> {
|
is LoadState.NotLoading -> {
|
||||||
if (loadState.append is LoadState.NotLoading) {
|
if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
|
||||||
binding.statusView.show()
|
binding.statusView.show()
|
||||||
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
|
binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
|
||||||
}
|
}
|
||||||
|
@ -238,7 +238,7 @@ class TimelineFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionButtonPresent()) {
|
if (actionButtonPresent()) {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
hideFab = preferences.getBoolean("fabHide", false)
|
hideFab = preferences.getBoolean("fabHide", false)
|
||||||
scrollListener = object : RecyclerView.OnScrollListener() {
|
scrollListener = object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
@ -435,7 +435,7 @@ class TimelineFragment :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onPreferenceChanged(key: String) {
|
private fun onPreferenceChanged(key: String) {
|
||||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
when (key) {
|
when (key) {
|
||||||
PrefKeys.FAB_HIDE -> {
|
PrefKeys.FAB_HIDE -> {
|
||||||
hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false)
|
hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false)
|
||||||
|
@ -502,7 +502,7 @@ class TimelineFragment :
|
||||||
* Auto dispose observable on pause
|
* Auto dispose observable on pause
|
||||||
*/
|
*/
|
||||||
private fun startUpdateTimestamp() {
|
private fun startUpdateTimestamp() {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(activity)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false)
|
val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false)
|
||||||
if (!useAbsoluteTime) {
|
if (!useAbsoluteTime) {
|
||||||
Observable.interval(1, TimeUnit.MINUTES)
|
Observable.interval(1, TimeUnit.MINUTES)
|
||||||
|
|
|
@ -114,7 +114,7 @@ class TimelinePagingAdapter(
|
||||||
oldItem: StatusViewData,
|
oldItem: StatusViewData,
|
||||||
newItem: StatusViewData
|
newItem: StatusViewData
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return oldItem.viewDataId == newItem.viewDataId
|
return oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
override fun areContentsTheSame(
|
||||||
|
@ -128,7 +128,7 @@ class TimelinePagingAdapter(
|
||||||
oldItem: StatusViewData,
|
oldItem: StatusViewData,
|
||||||
newItem: StatusViewData
|
newItem: StatusViewData
|
||||||
): Any? {
|
): Any? {
|
||||||
return if (oldItem === newItem) {
|
return if (oldItem == newItem) {
|
||||||
// If items are equal - update timestamp only
|
// If items are equal - update timestamp only
|
||||||
listOf(StatusBaseViewHolder.Key.KEY_CREATED)
|
listOf(StatusBaseViewHolder.Key.KEY_CREATED)
|
||||||
} else // If items are different - update the whole view holder
|
} else // If items are different - update the whole view holder
|
||||||
|
|
|
@ -23,12 +23,12 @@ import com.google.gson.reflect.TypeToken
|
||||||
import com.keylesspalace.tusky.db.TimelineAccountEntity
|
import com.keylesspalace.tusky.db.TimelineAccountEntity
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusEntity
|
import com.keylesspalace.tusky.db.TimelineStatusEntity
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||||
import com.keylesspalace.tusky.entity.Account
|
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
import com.keylesspalace.tusky.entity.HashTag
|
import com.keylesspalace.tusky.entity.HashTag
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
@ -44,7 +44,7 @@ private val emojisListType = object : TypeToken<List<Emoji>>() {}.type
|
||||||
private val mentionListType = object : TypeToken<List<Status.Mention>>() {}.type
|
private val mentionListType = object : TypeToken<List<Status.Mention>>() {}.type
|
||||||
private val tagListType = object : TypeToken<List<HashTag>>() {}.type
|
private val tagListType = object : TypeToken<List<HashTag>>() {}.type
|
||||||
|
|
||||||
fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
|
fun TimelineAccount.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
|
||||||
return TimelineAccountEntity(
|
return TimelineAccountEntity(
|
||||||
serverId = id,
|
serverId = id,
|
||||||
timelineUserId = accountId,
|
timelineUserId = accountId,
|
||||||
|
@ -58,25 +58,16 @@ fun Account.toEntity(accountId: Long, gson: Gson): TimelineAccountEntity {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun TimelineAccountEntity.toAccount(gson: Gson): Account {
|
fun TimelineAccountEntity.toAccount(gson: Gson): TimelineAccount {
|
||||||
return Account(
|
return TimelineAccount(
|
||||||
id = serverId,
|
id = serverId,
|
||||||
localUsername = localUsername,
|
localUsername = localUsername,
|
||||||
username = username,
|
username = username,
|
||||||
displayName = displayName,
|
displayName = displayName,
|
||||||
note = SpannedString(""),
|
|
||||||
url = url,
|
url = url,
|
||||||
avatar = avatar,
|
avatar = avatar,
|
||||||
header = "",
|
|
||||||
locked = false,
|
|
||||||
followingCount = 0,
|
|
||||||
followersCount = 0,
|
|
||||||
statusesCount = 0,
|
|
||||||
source = null,
|
|
||||||
bot = bot,
|
bot = bot,
|
||||||
emojis = gson.fromJson(emojis, emojisListType),
|
emojis = gson.fromJson(emojis, emojisListType)
|
||||||
fields = null,
|
|
||||||
moved = null
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.keylesspalace.tusky.components.timeline.util
|
||||||
|
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
fun Throwable.isExpected() = this is IOException || this is HttpException
|
||||||
|
|
||||||
|
inline fun <T> ifExpected(
|
||||||
|
t: Throwable,
|
||||||
|
cb: () -> T
|
||||||
|
): T {
|
||||||
|
if (t.isExpected()) {
|
||||||
|
return cb()
|
||||||
|
} else {
|
||||||
|
throw t
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,13 +23,13 @@ import androidx.room.withTransaction
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.keylesspalace.tusky.components.timeline.Placeholder
|
import com.keylesspalace.tusky.components.timeline.Placeholder
|
||||||
import com.keylesspalace.tusky.components.timeline.toEntity
|
import com.keylesspalace.tusky.components.timeline.toEntity
|
||||||
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusEntity
|
import com.keylesspalace.tusky.db.TimelineStatusEntity
|
||||||
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
import com.keylesspalace.tusky.db.TimelineStatusWithAccount
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.dec
|
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
|
||||||
|
@ -101,15 +101,22 @@ class CachedTimelineRemoteMediator(
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
val overlappedStatuses = replaceStatusRange(statuses, state)
|
val overlappedStatuses = replaceStatusRange(statuses, state)
|
||||||
|
|
||||||
if (loadType == LoadType.REFRESH && overlappedStatuses == 0 && statuses.isNotEmpty() && !dbEmpty) {
|
/* In case we loaded a whole page and there was no overlap with existing statuses,
|
||||||
|
we insert a placeholder because there might be even more unknown statuses */
|
||||||
|
if (loadType == LoadType.REFRESH && overlappedStatuses == 0 && statuses.size == state.config.pageSize && !dbEmpty) {
|
||||||
|
/* This overrides the last of the newly loaded statuses with a placeholder
|
||||||
|
to guarantee the placeholder has an id that exists on the server as not all
|
||||||
|
servers handle client generated ids as expected */
|
||||||
timelineDao.insertStatus(
|
timelineDao.insertStatus(
|
||||||
Placeholder(statuses.last().id.dec(), loading = false).toEntity(activeAccount.id)
|
Placeholder(statuses.last().id, loading = false).toEntity(activeAccount.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty())
|
return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return MediatorResult.Error(e)
|
return ifExpected(e) {
|
||||||
|
MediatorResult.Error(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,7 @@ import com.keylesspalace.tusky.appstore.ReblogEvent
|
||||||
import com.keylesspalace.tusky.components.timeline.Placeholder
|
import com.keylesspalace.tusky.components.timeline.Placeholder
|
||||||
import com.keylesspalace.tusky.components.timeline.toEntity
|
import com.keylesspalace.tusky.components.timeline.toEntity
|
||||||
import com.keylesspalace.tusky.components.timeline.toViewData
|
import com.keylesspalace.tusky.components.timeline.toViewData
|
||||||
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
|
@ -41,8 +42,6 @@ import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.FilterModel
|
import com.keylesspalace.tusky.network.FilterModel
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.TimelineCases
|
import com.keylesspalace.tusky.network.TimelineCases
|
||||||
import com.keylesspalace.tusky.util.dec
|
|
||||||
import com.keylesspalace.tusky.util.inc
|
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
@ -151,9 +150,11 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
|
|
||||||
timelineDao.insertStatus(Placeholder(placeholderId, loading = true).toEntity(activeAccount.id))
|
timelineDao.insertStatus(Placeholder(placeholderId, loading = true).toEntity(activeAccount.id))
|
||||||
|
|
||||||
val nextPlaceholderId = timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId)
|
val response = db.withTransaction {
|
||||||
|
val idAbovePlaceholder = timelineDao.getIdAbove(activeAccount.id, placeholderId)
|
||||||
val response = api.homeTimeline(maxId = placeholderId.inc(), sinceId = nextPlaceholderId, limit = LOAD_AT_ONCE).await()
|
val nextPlaceholderId = timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId)
|
||||||
|
api.homeTimeline(maxId = idAbovePlaceholder, sinceId = nextPlaceholderId, limit = LOAD_AT_ONCE)
|
||||||
|
}.await()
|
||||||
|
|
||||||
val statuses = response.body()
|
val statuses = response.body()
|
||||||
if (!response.isSuccessful || statuses == null) {
|
if (!response.isSuccessful || statuses == null) {
|
||||||
|
@ -187,14 +188,21 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (overlappedStatuses == 0 && statuses.isNotEmpty()) {
|
/* In case we loaded a whole page and there was no overlap with existing statuses,
|
||||||
|
we insert a placeholder because there might be even more unknown statuses */
|
||||||
|
if (overlappedStatuses == 0 && statuses.size == LOAD_AT_ONCE) {
|
||||||
|
/* This overrides the last of the newly loaded statuses with a placeholder
|
||||||
|
to guarantee the placeholder has an id that exists on the server as not all
|
||||||
|
servers handle client generated ids as expected */
|
||||||
timelineDao.insertStatus(
|
timelineDao.insertStatus(
|
||||||
Placeholder(statuses.last().id.dec(), loading = false).toEntity(activeAccount.id)
|
Placeholder(statuses.last().id, loading = false).toEntity(activeAccount.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
loadMoreFailed(placeholderId, e)
|
ifExpected(e) {
|
||||||
|
loadMoreFailed(placeholderId, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,9 +236,9 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
|
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
if (isFirstOfStreaming) {
|
if (isFirstOfStreaming) {
|
||||||
val placeholderId = status.id.dec()
|
timelineDao.insertStatus(Placeholder(status.id, loading = false).toEntity(activeAccount.id))
|
||||||
timelineDao.insertStatus(Placeholder(placeholderId, loading = false).toEntity(activeAccount.id))
|
|
||||||
isFirstOfStreaming = false
|
isFirstOfStreaming = false
|
||||||
|
return@withTransaction
|
||||||
}
|
}
|
||||||
|
|
||||||
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, gson))
|
timelineDao.insertAccount(status.account.toEntity(activeAccount.id, gson))
|
||||||
|
|
|
@ -19,9 +19,9 @@ import androidx.paging.ExperimentalPagingApi
|
||||||
import androidx.paging.LoadType
|
import androidx.paging.LoadType
|
||||||
import androidx.paging.PagingState
|
import androidx.paging.PagingState
|
||||||
import androidx.paging.RemoteMediator
|
import androidx.paging.RemoteMediator
|
||||||
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.util.HttpHeaderLink
|
import com.keylesspalace.tusky.util.HttpHeaderLink
|
||||||
import com.keylesspalace.tusky.util.dec
|
|
||||||
import com.keylesspalace.tusky.util.toViewData
|
import com.keylesspalace.tusky.util.toViewData
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
|
@ -92,7 +92,7 @@ class NetworkTimelineRemoteMediator(
|
||||||
viewModel.statusData.addAll(0, data)
|
viewModel.statusData.addAll(0, data)
|
||||||
|
|
||||||
if (insertPlaceholder) {
|
if (insertPlaceholder) {
|
||||||
viewModel.statusData.add(statuses.size, StatusViewData.Placeholder(statuses.last().id.dec(), false))
|
viewModel.statusData[statuses.size - 1] = StatusViewData.Placeholder(statuses.last().id, false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val linkHeader = statusResponse.headers()["Link"]
|
val linkHeader = statusResponse.headers()["Link"]
|
||||||
|
@ -107,7 +107,9 @@ class NetworkTimelineRemoteMediator(
|
||||||
viewModel.currentSource?.invalidate()
|
viewModel.currentSource?.invalidate()
|
||||||
return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty())
|
return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty())
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
return MediatorResult.Error(e)
|
return ifExpected(e) {
|
||||||
|
MediatorResult.Error(e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,15 +28,16 @@ import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.FavoriteEvent
|
import com.keylesspalace.tusky.appstore.FavoriteEvent
|
||||||
import com.keylesspalace.tusky.appstore.PinEvent
|
import com.keylesspalace.tusky.appstore.PinEvent
|
||||||
import com.keylesspalace.tusky.appstore.ReblogEvent
|
import com.keylesspalace.tusky.appstore.ReblogEvent
|
||||||
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.FilterModel
|
import com.keylesspalace.tusky.network.FilterModel
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.network.TimelineCases
|
import com.keylesspalace.tusky.network.TimelineCases
|
||||||
import com.keylesspalace.tusky.util.dec
|
|
||||||
import com.keylesspalace.tusky.util.getDomain
|
import com.keylesspalace.tusky.util.getDomain
|
||||||
import com.keylesspalace.tusky.util.inc
|
import com.keylesspalace.tusky.util.isLessThan
|
||||||
|
import com.keylesspalace.tusky.util.isLessThanOrEqual
|
||||||
import com.keylesspalace.tusky.util.toViewData
|
import com.keylesspalace.tusky.util.toViewData
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
@ -45,6 +46,7 @@ import kotlinx.coroutines.rx3.await
|
||||||
import net.accelf.yuito.streaming.StreamingManager
|
import net.accelf.yuito.streaming.StreamingManager
|
||||||
import retrofit2.HttpException
|
import retrofit2.HttpException
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
|
import java.io.IOException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -140,8 +142,10 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
statusData.indexOfFirst { it is StatusViewData.Placeholder && it.id == placeholderId }
|
statusData.indexOfFirst { it is StatusViewData.Placeholder && it.id == placeholderId }
|
||||||
statusData[placeholderIndex] = StatusViewData.Placeholder(placeholderId, isLoading = true)
|
statusData[placeholderIndex] = StatusViewData.Placeholder(placeholderId, isLoading = true)
|
||||||
|
|
||||||
|
val idAbovePlaceholder = statusData.getOrNull(placeholderIndex - 1)?.id
|
||||||
|
|
||||||
val statusResponse = fetchStatusesForKind(
|
val statusResponse = fetchStatusesForKind(
|
||||||
fromId = placeholderId.inc(),
|
fromId = idAbovePlaceholder,
|
||||||
uptoId = null,
|
uptoId = null,
|
||||||
limit = 20
|
limit = 20
|
||||||
)
|
)
|
||||||
|
@ -155,7 +159,7 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
statusData.removeAt(placeholderIndex)
|
statusData.removeAt(placeholderIndex)
|
||||||
|
|
||||||
val activeAccount = accountManager.activeAccount!!
|
val activeAccount = accountManager.activeAccount!!
|
||||||
val data = statuses.map { status ->
|
val data: MutableList<StatusViewData> = statuses.map { status ->
|
||||||
status.toViewData(
|
status.toViewData(
|
||||||
isShowingContent = activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
isShowingContent = activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive,
|
||||||
isExpanded = activeAccount.alwaysOpenSpoiler,
|
isExpanded = activeAccount.alwaysOpenSpoiler,
|
||||||
|
@ -164,30 +168,31 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
}.toMutableList()
|
}.toMutableList()
|
||||||
|
|
||||||
if (statuses.isNotEmpty()) {
|
if (statuses.isNotEmpty()) {
|
||||||
val firstId = statuses.first().id.hashCode().toLong()
|
val firstId = statuses.first().id
|
||||||
val lastId = statuses.last().id.hashCode().toLong()
|
val lastId = statuses.last().id
|
||||||
val overlappedFrom = statusData.indexOfFirst { it.viewDataId <= firstId }
|
val overlappedFrom = statusData.indexOfFirst { it.asStatusOrNull()?.id?.isLessThanOrEqual(firstId) ?: false }
|
||||||
val overlappedTo = statusData.indexOfFirst { it.viewDataId < lastId }
|
val overlappedTo = statusData.indexOfFirst { it.asStatusOrNull()?.id?.isLessThan(lastId) ?: false }
|
||||||
|
|
||||||
if (overlappedFrom < overlappedTo) {
|
if (overlappedFrom < overlappedTo) {
|
||||||
repeat(overlappedTo - overlappedFrom) {
|
data.mapIndexed { i, status -> i to statusData.firstOrNull { it.asStatusOrNull()?.id == status.id }?.asStatusOrNull() }
|
||||||
statusData[overlappedFrom].asStatusOrNull()?.let { oldStatus ->
|
.filter { (_, oldStatus) -> oldStatus != null }
|
||||||
val dataIndex = statuses.indexOfFirst { it.id == oldStatus.id }
|
.forEach { (i, oldStatus) ->
|
||||||
if (dataIndex == -1) {
|
data[i] = data[i].asStatusOrNull()!!
|
||||||
return@let
|
|
||||||
}
|
|
||||||
data[dataIndex] = data[dataIndex]
|
|
||||||
.copy(
|
.copy(
|
||||||
isShowingContent = oldStatus.isShowingContent,
|
isShowingContent = oldStatus!!.isShowingContent,
|
||||||
isExpanded = oldStatus.isExpanded,
|
isExpanded = oldStatus.isExpanded,
|
||||||
isCollapsed = oldStatus.isCollapsed,
|
isCollapsed = oldStatus.isCollapsed,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
statusData.removeAt(overlappedFrom)
|
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 {
|
} else {
|
||||||
statusData.add(overlappedFrom, StatusViewData.Placeholder(statuses.last().id.dec(), isLoading = false))
|
data[data.size - 1] = StatusViewData.Placeholder(statuses.last().id, isLoading = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,7 +200,9 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
|
|
||||||
currentSource?.invalidate()
|
currentSource?.invalidate()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
loadMoreFailed(placeholderId, e)
|
ifExpected(e) {
|
||||||
|
loadMoreFailed(placeholderId, e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -239,27 +246,27 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
val activeAccount = accountManager.activeAccount!!
|
val activeAccount = accountManager.activeAccount!!
|
||||||
|
|
||||||
if (isFirstOfStreaming) {
|
if (isFirstOfStreaming) {
|
||||||
val placeholderId = status.id.dec()
|
statusData.add(0, StatusViewData.Placeholder(status.id, isLoading = false))
|
||||||
statusData.add(0, StatusViewData.Placeholder(placeholderId, isLoading = false))
|
|
||||||
isFirstOfStreaming = false
|
isFirstOfStreaming = false
|
||||||
|
} else {
|
||||||
|
statusData.add(0, status.toViewData(
|
||||||
|
isShowingContent = activeAccount.alwaysShowSensitiveMedia,
|
||||||
|
isExpanded = activeAccount.alwaysOpenSpoiler,
|
||||||
|
isCollapsed = true,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
statusData.add(0, status.toViewData(
|
|
||||||
isShowingContent = activeAccount.alwaysShowSensitiveMedia,
|
|
||||||
isExpanded = activeAccount.alwaysOpenSpoiler,
|
|
||||||
isCollapsed = true,
|
|
||||||
))
|
|
||||||
|
|
||||||
currentSource?.invalidate()
|
currentSource?.invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun fullReload() {
|
override fun fullReload() {
|
||||||
nextKey = statusData.firstOrNull { it is StatusViewData.Concrete }?.asStatusOrNull()?.id?.inc()
|
nextKey = statusData.firstOrNull { it is StatusViewData.Concrete }?.asStatusOrNull()?.id
|
||||||
statusData.clear()
|
statusData.clear()
|
||||||
currentSource?.invalidate()
|
currentSource?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class, HttpException::class)
|
||||||
suspend fun fetchStatusesForKind(
|
suspend fun fetchStatusesForKind(
|
||||||
fromId: String?,
|
fromId: String?,
|
||||||
uptoId: String?,
|
uptoId: String?,
|
||||||
|
|
|
@ -20,7 +20,21 @@ import android.util.Log
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import com.keylesspalace.tusky.appstore.*
|
import com.keylesspalace.tusky.appstore.BlockEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.BookmarkEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.DomainMuteEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.Event
|
||||||
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
|
import com.keylesspalace.tusky.appstore.FavoriteEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.MuteConversationEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.MuteEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.PinEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.ReblogEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.StatusDeletedEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.StreamUpdateEvent
|
||||||
|
import com.keylesspalace.tusky.appstore.UnfollowEvent
|
||||||
|
import com.keylesspalace.tusky.components.timeline.util.ifExpected
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.entity.Filter
|
import com.keylesspalace.tusky.entity.Filter
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
|
@ -38,8 +52,6 @@ import kotlinx.coroutines.rx3.await
|
||||||
import net.accelf.yuito.streaming.StreamType
|
import net.accelf.yuito.streaming.StreamType
|
||||||
import net.accelf.yuito.streaming.StreamingManager
|
import net.accelf.yuito.streaming.StreamingManager
|
||||||
import net.accelf.yuito.streaming.Subscription
|
import net.accelf.yuito.streaming.Subscription
|
||||||
import retrofit2.HttpException
|
|
||||||
import java.io.IOException
|
|
||||||
|
|
||||||
abstract class TimelineViewModel(
|
abstract class TimelineViewModel(
|
||||||
private val timelineCases: TimelineCases,
|
private val timelineCases: TimelineCases,
|
||||||
|
@ -344,19 +356,6 @@ abstract class TimelineViewModel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isExpectedRequestException(t: Exception) = t is IOException || t is HttpException
|
|
||||||
|
|
||||||
private inline fun ifExpected(
|
|
||||||
t: Exception,
|
|
||||||
cb: () -> Unit,
|
|
||||||
) {
|
|
||||||
if (isExpectedRequestException(t)) {
|
|
||||||
cb()
|
|
||||||
} else {
|
|
||||||
throw t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "TimelineVM"
|
private const val TAG = "TimelineVM"
|
||||||
internal const val LOAD_AT_ONCE = 30
|
internal const val LOAD_AT_ONCE = 30
|
||||||
|
|
|
@ -29,10 +29,9 @@ import java.io.File;
|
||||||
/**
|
/**
|
||||||
* DB version & declare DAO
|
* DB version & declare DAO
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||||
TimelineAccountEntity.class, ConversationEntity.class
|
TimelineAccountEntity.class, ConversationEntity.class
|
||||||
}, version = 30)
|
}, version = 31)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public abstract AccountDao accountDao();
|
public abstract AccountDao accountDao();
|
||||||
|
@ -474,4 +473,14 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollDuration` 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`");
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,6 +186,15 @@ AND timelineUserId = :accountId
|
||||||
@Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1")
|
@Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1")
|
||||||
abstract suspend fun getTopPlaceholderId(accountId: Long): String?
|
abstract suspend fun getTopPlaceholderId(accountId: Long): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the id directly above [serverId], or null if [serverId] is the id of the top status
|
||||||
|
*/
|
||||||
|
@Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND (LENGTH(:serverId) < LENGTH(serverId) OR (LENGTH(:serverId) = LENGTH(serverId) AND :serverId < serverId)) ORDER BY LENGTH(serverId) ASC, serverId ASC LIMIT 1")
|
||||||
|
abstract suspend fun getIdAbove(accountId: Long, serverId: String): String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the id of the next placeholder after [serverId]
|
||||||
|
*/
|
||||||
@Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL AND (LENGTH(:serverId) > LENGTH(serverId) OR (LENGTH(:serverId) = LENGTH(serverId) AND :serverId > serverId)) ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1")
|
@Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL AND (LENGTH(:serverId) > LENGTH(serverId) OR (LENGTH(:serverId) = LENGTH(serverId) AND :serverId > serverId)) ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1")
|
||||||
abstract suspend fun getNextPlaceholderIdAfter(accountId: Long, serverId: String): String?
|
abstract suspend fun getNextPlaceholderIdAfter(accountId: Long, serverId: String): String?
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,9 +22,7 @@ import com.keylesspalace.tusky.EditProfileActivity
|
||||||
import com.keylesspalace.tusky.FiltersActivity
|
import com.keylesspalace.tusky.FiltersActivity
|
||||||
import com.keylesspalace.tusky.LicenseActivity
|
import com.keylesspalace.tusky.LicenseActivity
|
||||||
import com.keylesspalace.tusky.ListsActivity
|
import com.keylesspalace.tusky.ListsActivity
|
||||||
import com.keylesspalace.tusky.LoginActivity
|
|
||||||
import com.keylesspalace.tusky.MainActivity
|
import com.keylesspalace.tusky.MainActivity
|
||||||
import com.keylesspalace.tusky.SplashActivity
|
|
||||||
import com.keylesspalace.tusky.StatusListActivity
|
import com.keylesspalace.tusky.StatusListActivity
|
||||||
import com.keylesspalace.tusky.TabPreferenceActivity
|
import com.keylesspalace.tusky.TabPreferenceActivity
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity
|
import com.keylesspalace.tusky.ViewMediaActivity
|
||||||
|
@ -34,9 +32,11 @@ import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
||||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||||
|
import com.keylesspalace.tusky.components.login.LoginActivity
|
||||||
|
import com.keylesspalace.tusky.components.login.LoginWebViewActivity
|
||||||
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
||||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||||
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
|
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity
|
||||||
import com.keylesspalace.tusky.components.search.SearchActivity
|
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
@ -86,7 +86,7 @@ abstract class ActivitiesModule {
|
||||||
abstract fun contributesLoginActivity(): LoginActivity
|
abstract fun contributesLoginActivity(): LoginActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesSplashActivity(): SplashActivity
|
abstract fun contributesLoginWebViewActivity(): LoginWebViewActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
|
||||||
abstract fun contributesPreferencesActivity(): PreferencesActivity
|
abstract fun contributesPreferencesActivity(): PreferencesActivity
|
||||||
|
@ -110,7 +110,7 @@ abstract class ActivitiesModule {
|
||||||
abstract fun contributesInstanceListActivity(): InstanceListActivity
|
abstract fun contributesInstanceListActivity(): InstanceListActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesScheduledTootActivity(): ScheduledTootActivity
|
abstract fun contributesScheduledStatusActivity(): ScheduledStatusActivity
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity
|
abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity
|
||||||
|
|
|
@ -68,7 +68,7 @@ class AppModule {
|
||||||
AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25,
|
AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25,
|
||||||
AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")),
|
AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")),
|
||||||
AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29,
|
AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29,
|
||||||
AppDatabase.MIGRATION_29_30
|
AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,12 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.di
|
package com.keylesspalace.tusky.di
|
||||||
|
|
||||||
import com.keylesspalace.tusky.service.SendTootService
|
import com.keylesspalace.tusky.service.SendStatusService
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
abstract class ServicesModule {
|
abstract class ServicesModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesSendTootService(): SendTootService
|
abstract fun contributesSendStatusService(): SendStatusService
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftsViewModel
|
import com.keylesspalace.tusky.components.drafts.DraftsViewModel
|
||||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||||
import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel
|
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusViewModel
|
||||||
import com.keylesspalace.tusky.components.search.SearchViewModel
|
import com.keylesspalace.tusky.components.search.SearchViewModel
|
||||||
import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewModel
|
import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewModel
|
||||||
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel
|
||||||
|
@ -86,8 +86,8 @@ abstract class ViewModelModule {
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
@ViewModelKey(ScheduledTootViewModel::class)
|
@ViewModelKey(ScheduledStatusViewModel::class)
|
||||||
internal abstract fun scheduledTootViewModel(viewModel: ScheduledTootViewModel): ViewModel
|
internal abstract fun scheduledStatusViewModel(viewModel: ScheduledStatusViewModel): ViewModel
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
@IntoMap
|
@IntoMap
|
||||||
|
|
|
@ -17,7 +17,7 @@ package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import java.util.*
|
import java.util.Date
|
||||||
|
|
||||||
data class Account(
|
data class Account(
|
||||||
val id: String,
|
val id: String,
|
||||||
|
@ -53,37 +53,57 @@ data class Account(
|
||||||
val intentionallyUseDisplayName: String
|
val intentionallyUseDisplayName: String
|
||||||
get() = displayName.orEmpty()
|
get() = displayName.orEmpty()
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
|
||||||
return id.hashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (other !is Account) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return other.id == this.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deepEquals(other: Account): Boolean {
|
|
||||||
return id == other.id &&
|
|
||||||
localUsername == other.localUsername &&
|
|
||||||
displayName == other.displayName &&
|
|
||||||
note == other.note &&
|
|
||||||
url == other.url &&
|
|
||||||
avatar == other.avatar &&
|
|
||||||
header == other.header &&
|
|
||||||
locked == other.locked &&
|
|
||||||
followersCount == other.followersCount &&
|
|
||||||
followingCount == other.followingCount &&
|
|
||||||
statusesCount == other.statusesCount &&
|
|
||||||
source == other.source &&
|
|
||||||
bot == other.bot &&
|
|
||||||
emojis == other.emojis &&
|
|
||||||
fields == other.fields &&
|
|
||||||
moved == other.moved
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isRemote(): Boolean = this.username != this.localUsername
|
fun isRemote(): Boolean = this.username != this.localUsername
|
||||||
|
|
||||||
|
/**
|
||||||
|
* overriding equals & hashcode because Spanned does not always compare correctly otherwise
|
||||||
|
*/
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
other as Account
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (localUsername != other.localUsername) return false
|
||||||
|
if (username != other.username) return false
|
||||||
|
if (displayName != other.displayName) return false
|
||||||
|
if (note.toString() != other.note.toString()) return false
|
||||||
|
if (url != other.url) return false
|
||||||
|
if (avatar != other.avatar) return false
|
||||||
|
if (header != other.header) return false
|
||||||
|
if (locked != other.locked) return false
|
||||||
|
if (followersCount != other.followersCount) return false
|
||||||
|
if (followingCount != other.followingCount) return false
|
||||||
|
if (statusesCount != other.statusesCount) return false
|
||||||
|
if (source != other.source) return false
|
||||||
|
if (bot != other.bot) return false
|
||||||
|
if (emojis != other.emojis) return false
|
||||||
|
if (fields != other.fields) return false
|
||||||
|
if (moved != other.moved) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id.hashCode()
|
||||||
|
result = 31 * result + localUsername.hashCode()
|
||||||
|
result = 31 * result + username.hashCode()
|
||||||
|
result = 31 * result + (displayName?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + note.toString().hashCode()
|
||||||
|
result = 31 * result + url.hashCode()
|
||||||
|
result = 31 * result + avatar.hashCode()
|
||||||
|
result = 31 * result + header.hashCode()
|
||||||
|
result = 31 * result + locked.hashCode()
|
||||||
|
result = 31 * result + followersCount
|
||||||
|
result = 31 * result + followingCount
|
||||||
|
result = 31 * result + statusesCount
|
||||||
|
result = 31 * result + (source?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + bot.hashCode()
|
||||||
|
result = 31 * result + (emojis?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (fields?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (moved?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class AccountSource(
|
data class AccountSource(
|
||||||
|
|
|
@ -19,7 +19,7 @@ import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
data class Conversation(
|
data class Conversation(
|
||||||
val id: String,
|
val id: String,
|
||||||
val accounts: List<Account>,
|
val accounts: List<TimelineAccount>,
|
||||||
@SerializedName("last_status") val lastStatus: Status?, // should never be null, but apparently its possible https://github.com/tuskyapp/Tusky/issues/1038
|
@SerializedName("last_status") val lastStatus: Status?, // should never be null, but apparently its possible https://github.com/tuskyapp/Tusky/issues/1038
|
||||||
val unread: Boolean
|
val unread: Boolean
|
||||||
)
|
)
|
||||||
|
|
|
@ -24,7 +24,7 @@ import com.google.gson.annotations.JsonAdapter
|
||||||
data class Notification(
|
data class Notification(
|
||||||
val type: Type,
|
val type: Type,
|
||||||
val id: String,
|
val id: String,
|
||||||
val account: Account,
|
val account: TimelineAccount,
|
||||||
val status: Status?
|
val status: Status?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
data class SearchResult(
|
data class SearchResult(
|
||||||
val accounts: List<Account>,
|
val accounts: List<TimelineAccount>,
|
||||||
val statuses: List<Status>,
|
val statuses: List<Status>,
|
||||||
val hashtags: List<HashTag>
|
val hashtags: List<HashTag>
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,7 +25,7 @@ import java.util.Date
|
||||||
data class Status(
|
data class Status(
|
||||||
val id: String,
|
val id: String,
|
||||||
val url: String?, // not present if it's reblog
|
val url: String?, // not present if it's reblog
|
||||||
val account: Account,
|
val account: TimelineAccount,
|
||||||
@SerializedName("in_reply_to_id") var inReplyToId: String?,
|
@SerializedName("in_reply_to_id") var inReplyToId: String?,
|
||||||
@SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?,
|
@SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?,
|
||||||
val reblog: Status?,
|
val reblog: Status?,
|
||||||
|
@ -158,6 +158,71 @@ data class Status(
|
||||||
return builder.toString()
|
return builder.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* overriding equals & hashcode because Spanned does not always compare correctly otherwise
|
||||||
|
*/
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
other as Status
|
||||||
|
|
||||||
|
if (id != other.id) return false
|
||||||
|
if (url != other.url) return false
|
||||||
|
if (account != other.account) return false
|
||||||
|
if (inReplyToId != other.inReplyToId) return false
|
||||||
|
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
||||||
|
if (reblog != other.reblog) return false
|
||||||
|
if (content.toString() != other.content.toString()) return false
|
||||||
|
if (createdAt != other.createdAt) return false
|
||||||
|
if (emojis != other.emojis) return false
|
||||||
|
if (reblogsCount != other.reblogsCount) return false
|
||||||
|
if (favouritesCount != other.favouritesCount) return false
|
||||||
|
if (reblogged != other.reblogged) return false
|
||||||
|
if (favourited != other.favourited) return false
|
||||||
|
if (bookmarked != other.bookmarked) return false
|
||||||
|
if (sensitive != other.sensitive) return false
|
||||||
|
if (spoilerText != other.spoilerText) return false
|
||||||
|
if (visibility != other.visibility) return false
|
||||||
|
if (attachments != other.attachments) return false
|
||||||
|
if (mentions != other.mentions) return false
|
||||||
|
if (tags != other.tags) return false
|
||||||
|
if (application != other.application) return false
|
||||||
|
if (pinned != other.pinned) return false
|
||||||
|
if (muted != other.muted) return false
|
||||||
|
if (poll != other.poll) return false
|
||||||
|
if (card != other.card) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = id.hashCode()
|
||||||
|
result = 31 * result + (url?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + account.hashCode()
|
||||||
|
result = 31 * result + (inReplyToId?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (reblog?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + content.toString().hashCode()
|
||||||
|
result = 31 * result + createdAt.hashCode()
|
||||||
|
result = 31 * result + emojis.hashCode()
|
||||||
|
result = 31 * result + reblogsCount
|
||||||
|
result = 31 * result + favouritesCount
|
||||||
|
result = 31 * result + reblogged.hashCode()
|
||||||
|
result = 31 * result + favourited.hashCode()
|
||||||
|
result = 31 * result + bookmarked.hashCode()
|
||||||
|
result = 31 * result + sensitive.hashCode()
|
||||||
|
result = 31 * result + spoilerText.hashCode()
|
||||||
|
result = 31 * result + visibility.hashCode()
|
||||||
|
result = 31 * result + attachments.hashCode()
|
||||||
|
result = 31 * result + mentions.hashCode()
|
||||||
|
result = 31 * result + (tags?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (application?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (pinned?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (muted?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (poll?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (card?.hashCode() ?: 0)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
data class Mention(
|
data class Mention(
|
||||||
val id: String,
|
val id: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
/* 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
|
||||||
|
@SerializedName("name") val notestockUsername: String? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val name: String
|
||||||
|
get() = if (displayName.isNullOrEmpty()) {
|
||||||
|
localUsername
|
||||||
|
} else displayName
|
||||||
|
}
|
|
@ -42,8 +42,8 @@ import com.keylesspalace.tusky.components.account.AccountActivity
|
||||||
import com.keylesspalace.tusky.databinding.FragmentAccountListBinding
|
import com.keylesspalace.tusky.databinding.FragmentAccountListBinding
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.entity.Account
|
|
||||||
import com.keylesspalace.tusky.entity.Relationship
|
import com.keylesspalace.tusky.entity.Relationship
|
||||||
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
import com.keylesspalace.tusky.interfaces.AccountActionListener
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
import com.keylesspalace.tusky.settings.PrefKeys
|
||||||
|
@ -255,7 +255,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
|
||||||
followRequestsAdapter.removeItem(position)
|
followRequestsAdapter.removeItem(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFetchCallByListType(fromId: String?): Single<Response<List<Account>>> {
|
private fun getFetchCallByListType(fromId: String?): Single<Response<List<TimelineAccount>>> {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
Type.FOLLOWS -> {
|
Type.FOLLOWS -> {
|
||||||
val accountId = requireId(type, id)
|
val accountId = requireId(type, id)
|
||||||
|
@ -313,7 +313,7 @@ class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountAct
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFetchAccountsSuccess(accounts: List<Account>, linkHeader: String?) {
|
private fun onFetchAccountsSuccess(accounts: List<TimelineAccount>, linkHeader: String?) {
|
||||||
adapter.setBottomLoading(false)
|
adapter.setBottomLoading(false)
|
||||||
|
|
||||||
val links = HttpHeaderLink.parse(linkHeader)
|
val links = HttpHeaderLink.parse(linkHeader)
|
||||||
|
|
|
@ -243,7 +243,7 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
|
|
||||||
popup.setOnMenuItemClickListener(item -> {
|
popup.setOnMenuItemClickListener(item -> {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.status_share_content: {
|
case R.id.post_share_content: {
|
||||||
Status statusToShare = status;
|
Status statusToShare = status;
|
||||||
if (statusToShare.getReblog() != null)
|
if (statusToShare.getReblog() != null)
|
||||||
statusToShare = statusToShare.getReblog();
|
statusToShare = statusToShare.getReblog();
|
||||||
|
@ -257,15 +257,15 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare);
|
sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare);
|
||||||
sendIntent.putExtra(Intent.EXTRA_SUBJECT, statusUrl);
|
sendIntent.putExtra(Intent.EXTRA_SUBJECT, statusUrl);
|
||||||
sendIntent.setType("text/plain");
|
sendIntent.setType("text/plain");
|
||||||
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_content_to)));
|
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_post_content_to)));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case R.id.status_share_link: {
|
case R.id.post_share_link: {
|
||||||
Intent sendIntent = new Intent();
|
Intent sendIntent = new Intent();
|
||||||
sendIntent.setAction(Intent.ACTION_SEND);
|
sendIntent.setAction(Intent.ACTION_SEND);
|
||||||
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl);
|
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl);
|
||||||
sendIntent.setType("text/plain");
|
sendIntent.setType("text/plain");
|
||||||
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_link_to)));
|
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_post_link_to)));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
case R.id.status_copy_link: {
|
case R.id.status_copy_link: {
|
||||||
|
@ -407,7 +407,7 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
|
|
||||||
protected void showConfirmDeleteDialog(final String id, final int position) {
|
protected void showConfirmDeleteDialog(final String id, final int position) {
|
||||||
new AlertDialog.Builder(getActivity())
|
new AlertDialog.Builder(getActivity())
|
||||||
.setMessage(R.string.dialog_delete_toot_warning)
|
.setMessage(R.string.dialog_delete_post_warning)
|
||||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
|
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
|
||||||
timelineCases.delete(id)
|
timelineCases.delete(id)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -430,7 +430,7 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
new AlertDialog.Builder(getActivity())
|
new AlertDialog.Builder(getActivity())
|
||||||
.setMessage(R.string.dialog_redraft_toot_warning)
|
.setMessage(R.string.dialog_redraft_post_warning)
|
||||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
|
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> {
|
||||||
timelineCases.delete(id)
|
timelineCases.delete(id)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
@ -442,7 +442,7 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
deletedStatus = status.toDeletedStatus();
|
deletedStatus = status.toDeletedStatus();
|
||||||
}
|
}
|
||||||
ComposeOptions composeOptions = new ComposeOptions();
|
ComposeOptions composeOptions = new ComposeOptions();
|
||||||
composeOptions.setTootText(deletedStatus.getText());
|
composeOptions.setContent(deletedStatus.getText());
|
||||||
composeOptions.setInReplyToId(deletedStatus.getInReplyToId());
|
composeOptions.setInReplyToId(deletedStatus.getInReplyToId());
|
||||||
composeOptions.setVisibility(deletedStatus.getVisibility());
|
composeOptions.setVisibility(deletedStatus.getVisibility());
|
||||||
composeOptions.setContentWarning(deletedStatus.getSpoilerText());
|
composeOptions.setContentWarning(deletedStatus.getSpoilerText());
|
||||||
|
|
|
@ -16,8 +16,10 @@
|
||||||
package com.keylesspalace.tusky.json
|
package com.keylesspalace.tusky.json
|
||||||
|
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
|
import android.text.SpannedString
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.core.text.parseAsHtml
|
import androidx.core.text.parseAsHtml
|
||||||
|
import androidx.core.text.toHtml
|
||||||
import com.google.gson.JsonDeserializationContext
|
import com.google.gson.JsonDeserializationContext
|
||||||
import com.google.gson.JsonDeserializer
|
import com.google.gson.JsonDeserializer
|
||||||
import com.google.gson.JsonElement
|
import com.google.gson.JsonElement
|
||||||
|
@ -32,16 +34,29 @@ import java.lang.reflect.Type
|
||||||
class SpannedTypeAdapter : JsonDeserializer<Spanned>, JsonSerializer<Spanned?> {
|
class SpannedTypeAdapter : JsonDeserializer<Spanned>, JsonSerializer<Spanned?> {
|
||||||
@Throws(JsonParseException::class)
|
@Throws(JsonParseException::class)
|
||||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Spanned {
|
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Spanned {
|
||||||
/* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which
|
return json.asString
|
||||||
* all status contents do, so it should be trimmed. */
|
/* Mastodon uses 'white-space: pre-wrap;' so spaces are displayed as returned by the Api.
|
||||||
return Jsoup.parse(json.asString ?: "")
|
* We can't use CSS so we replace spaces with non-breaking-spaces to emulate the behavior.
|
||||||
.apply {
|
*/
|
||||||
select(".quote-inline").forEach { it.remove() }
|
?.replace("<br> ", "<br> ")
|
||||||
}
|
?.replace("<br /> ", "<br /> ")
|
||||||
.html().parseAsHtml().trimTrailingWhitespace()
|
?.replace("<br/> ", "<br/> ")
|
||||||
|
?.replace(" ", " ")
|
||||||
|
?.let { html ->
|
||||||
|
Jsoup.parse(html)
|
||||||
|
.apply {
|
||||||
|
select(".quote-inline").forEach { it.remove() }
|
||||||
|
}
|
||||||
|
.html()
|
||||||
|
}
|
||||||
|
?.parseAsHtml()
|
||||||
|
/* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which
|
||||||
|
* most status contents do, so it should be trimmed. */
|
||||||
|
?.trimTrailingWhitespace()
|
||||||
|
?: SpannedString("")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun serialize(src: Spanned?, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
|
override fun serialize(src: Spanned?, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
|
||||||
return JsonPrimitive(HtmlCompat.toHtml(src!!, HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL))
|
return JsonPrimitive(src!!.toHtml(HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||||
import com.keylesspalace.tusky.entity.SearchResult
|
import com.keylesspalace.tusky.entity.SearchResult
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.entity.StatusContext
|
import com.keylesspalace.tusky.entity.StatusContext
|
||||||
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import io.reactivex.rxjava3.core.Completable
|
import io.reactivex.rxjava3.core.Completable
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
|
@ -178,13 +179,13 @@ interface MastodonApi {
|
||||||
fun statusRebloggedBy(
|
fun statusRebloggedBy(
|
||||||
@Path("id") statusId: String,
|
@Path("id") statusId: String,
|
||||||
@Query("max_id") maxId: String?
|
@Query("max_id") maxId: String?
|
||||||
): Single<Response<List<Account>>>
|
): Single<Response<List<TimelineAccount>>>
|
||||||
|
|
||||||
@GET("api/v1/statuses/{id}/favourited_by")
|
@GET("api/v1/statuses/{id}/favourited_by")
|
||||||
fun statusFavouritedBy(
|
fun statusFavouritedBy(
|
||||||
@Path("id") statusId: String,
|
@Path("id") statusId: String,
|
||||||
@Query("max_id") maxId: String?
|
@Query("max_id") maxId: String?
|
||||||
): Single<Response<List<Account>>>
|
): Single<Response<List<TimelineAccount>>>
|
||||||
|
|
||||||
@DELETE("api/v1/statuses/{id}")
|
@DELETE("api/v1/statuses/{id}")
|
||||||
fun deleteStatus(
|
fun deleteStatus(
|
||||||
|
@ -286,7 +287,7 @@ interface MastodonApi {
|
||||||
@Query("resolve") resolve: Boolean? = null,
|
@Query("resolve") resolve: Boolean? = null,
|
||||||
@Query("limit") limit: Int? = null,
|
@Query("limit") limit: Int? = null,
|
||||||
@Query("following") following: Boolean? = null
|
@Query("following") following: Boolean? = null
|
||||||
): Single<List<Account>>
|
): Single<List<TimelineAccount>>
|
||||||
|
|
||||||
@GET("api/v1/accounts/{id}")
|
@GET("api/v1/accounts/{id}")
|
||||||
fun account(
|
fun account(
|
||||||
|
@ -317,13 +318,13 @@ interface MastodonApi {
|
||||||
fun accountFollowers(
|
fun accountFollowers(
|
||||||
@Path("id") accountId: String,
|
@Path("id") accountId: String,
|
||||||
@Query("max_id") maxId: String?
|
@Query("max_id") maxId: String?
|
||||||
): Single<Response<List<Account>>>
|
): Single<Response<List<TimelineAccount>>>
|
||||||
|
|
||||||
@GET("api/v1/accounts/{id}/following")
|
@GET("api/v1/accounts/{id}/following")
|
||||||
fun accountFollowing(
|
fun accountFollowing(
|
||||||
@Path("id") accountId: String,
|
@Path("id") accountId: String,
|
||||||
@Query("max_id") maxId: String?
|
@Query("max_id") maxId: String?
|
||||||
): Single<Response<List<Account>>>
|
): Single<Response<List<TimelineAccount>>>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("api/v1/accounts/{id}/follow")
|
@POST("api/v1/accounts/{id}/follow")
|
||||||
|
@ -384,12 +385,12 @@ interface MastodonApi {
|
||||||
@GET("api/v1/blocks")
|
@GET("api/v1/blocks")
|
||||||
fun blocks(
|
fun blocks(
|
||||||
@Query("max_id") maxId: String?
|
@Query("max_id") maxId: String?
|
||||||
): Single<Response<List<Account>>>
|
): Single<Response<List<TimelineAccount>>>
|
||||||
|
|
||||||
@GET("api/v1/mutes")
|
@GET("api/v1/mutes")
|
||||||
fun mutes(
|
fun mutes(
|
||||||
@Query("max_id") maxId: String?
|
@Query("max_id") maxId: String?
|
||||||
): Single<Response<List<Account>>>
|
): Single<Response<List<TimelineAccount>>>
|
||||||
|
|
||||||
@GET("api/v1/domain_blocks")
|
@GET("api/v1/domain_blocks")
|
||||||
fun domainBlocks(
|
fun domainBlocks(
|
||||||
|
@ -426,7 +427,7 @@ interface MastodonApi {
|
||||||
@GET("api/v1/follow_requests")
|
@GET("api/v1/follow_requests")
|
||||||
fun followRequests(
|
fun followRequests(
|
||||||
@Query("max_id") maxId: String?
|
@Query("max_id") maxId: String?
|
||||||
): Single<Response<List<Account>>>
|
): Single<Response<List<TimelineAccount>>>
|
||||||
|
|
||||||
@POST("api/v1/follow_requests/{id}/authorize")
|
@POST("api/v1/follow_requests/{id}/authorize")
|
||||||
fun authorizeFollowRequest(
|
fun authorizeFollowRequest(
|
||||||
|
@ -440,24 +441,24 @@ interface MastodonApi {
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("api/v1/apps")
|
@POST("api/v1/apps")
|
||||||
fun authenticateApp(
|
suspend fun authenticateApp(
|
||||||
@Header(DOMAIN_HEADER) domain: String,
|
@Header(DOMAIN_HEADER) domain: String,
|
||||||
@Field("client_name") clientName: String,
|
@Field("client_name") clientName: String,
|
||||||
@Field("redirect_uris") redirectUris: String,
|
@Field("redirect_uris") redirectUris: String,
|
||||||
@Field("scopes") scopes: String,
|
@Field("scopes") scopes: String,
|
||||||
@Field("website") website: String
|
@Field("website") website: String
|
||||||
): Call<AppCredentials>
|
): AppCredentials
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("oauth/token")
|
@POST("oauth/token")
|
||||||
fun fetchOAuthToken(
|
suspend fun fetchOAuthToken(
|
||||||
@Header(DOMAIN_HEADER) domain: String,
|
@Header(DOMAIN_HEADER) domain: String,
|
||||||
@Field("client_id") clientId: String,
|
@Field("client_id") clientId: String,
|
||||||
@Field("client_secret") clientSecret: String,
|
@Field("client_secret") clientSecret: String,
|
||||||
@Field("redirect_uri") redirectUri: String,
|
@Field("redirect_uri") redirectUri: String,
|
||||||
@Field("code") code: String,
|
@Field("code") code: String,
|
||||||
@Field("grant_type") grantType: String
|
@Field("grant_type") grantType: String
|
||||||
): Call<AccessToken>
|
): AccessToken
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("api/v1/lists")
|
@POST("api/v1/lists")
|
||||||
|
@ -481,7 +482,7 @@ interface MastodonApi {
|
||||||
fun getAccountsInList(
|
fun getAccountsInList(
|
||||||
@Path("listId") listId: String,
|
@Path("listId") listId: String,
|
||||||
@Query("limit") limit: Int
|
@Query("limit") limit: Int
|
||||||
): Single<List<Account>>
|
): Single<List<TimelineAccount>>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
// @DELETE doesn't support fields
|
// @DELETE doesn't support fields
|
||||||
|
|
|
@ -18,19 +18,19 @@ package com.keylesspalace.tusky.receiver
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Color
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.app.RemoteInput
|
import androidx.core.app.RemoteInput
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions
|
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.service.SendTootService
|
import com.keylesspalace.tusky.service.SendStatusService
|
||||||
import com.keylesspalace.tusky.service.TootToSend
|
import com.keylesspalace.tusky.service.StatusToSend
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
import dagger.android.AndroidInjection
|
import dagger.android.AndroidInjection
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -45,22 +45,19 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
AndroidInjection.inject(this, context)
|
AndroidInjection.inject(this, context)
|
||||||
|
|
||||||
val notificationId = intent.getIntExtra(NotificationHelper.KEY_NOTIFICATION_ID, -1)
|
|
||||||
val senderId = intent.getLongExtra(NotificationHelper.KEY_SENDER_ACCOUNT_ID, -1)
|
|
||||||
val senderIdentifier = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_IDENTIFIER)
|
|
||||||
val senderFullName = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_FULL_NAME)
|
|
||||||
val citedStatusId = intent.getStringExtra(NotificationHelper.KEY_CITED_STATUS_ID)
|
|
||||||
val visibility = intent.getSerializableExtra(NotificationHelper.KEY_VISIBILITY) as Status.Visibility
|
|
||||||
val spoiler = intent.getStringExtra(NotificationHelper.KEY_SPOILER) ?: ""
|
|
||||||
val mentions = intent.getStringArrayExtra(NotificationHelper.KEY_MENTIONS) ?: emptyArray()
|
|
||||||
val citedText = intent.getStringExtra(NotificationHelper.KEY_CITED_TEXT)
|
|
||||||
val localAuthorId = intent.getStringExtra(NotificationHelper.KEY_CITED_AUTHOR_LOCAL)
|
|
||||||
|
|
||||||
val account = accountManager.getAccountById(senderId)
|
|
||||||
|
|
||||||
val notificationManager = NotificationManagerCompat.from(context)
|
|
||||||
|
|
||||||
if (intent.action == NotificationHelper.REPLY_ACTION) {
|
if (intent.action == NotificationHelper.REPLY_ACTION) {
|
||||||
|
val notificationId = intent.getIntExtra(NotificationHelper.KEY_NOTIFICATION_ID, -1)
|
||||||
|
val senderId = intent.getLongExtra(NotificationHelper.KEY_SENDER_ACCOUNT_ID, -1)
|
||||||
|
val senderIdentifier = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_IDENTIFIER)
|
||||||
|
val senderFullName = intent.getStringExtra(NotificationHelper.KEY_SENDER_ACCOUNT_FULL_NAME)
|
||||||
|
val citedStatusId = intent.getStringExtra(NotificationHelper.KEY_CITED_STATUS_ID)
|
||||||
|
val visibility = intent.getSerializableExtra(NotificationHelper.KEY_VISIBILITY) as Status.Visibility
|
||||||
|
val spoiler = intent.getStringExtra(NotificationHelper.KEY_SPOILER) ?: ""
|
||||||
|
val mentions = intent.getStringArrayExtra(NotificationHelper.KEY_MENTIONS) ?: emptyArray()
|
||||||
|
|
||||||
|
val account = accountManager.getAccountById(senderId)
|
||||||
|
|
||||||
|
val notificationManager = NotificationManagerCompat.from(context)
|
||||||
|
|
||||||
val message = getReplyMessage(intent)
|
val message = getReplyMessage(intent)
|
||||||
|
|
||||||
|
@ -85,9 +82,9 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
||||||
} else {
|
} else {
|
||||||
val text = mentions.joinToString(" ", postfix = " ") { "@$it" } + message.toString()
|
val text = mentions.joinToString(" ", postfix = " ") { "@$it" } + message.toString()
|
||||||
|
|
||||||
val sendIntent = SendTootService.sendTootIntent(
|
val sendIntent = SendStatusService.sendStatusIntent(
|
||||||
context,
|
context,
|
||||||
TootToSend(
|
StatusToSend(
|
||||||
text = text,
|
text = text,
|
||||||
warningText = spoiler,
|
warningText = spoiler,
|
||||||
visibility = visibility.serverString(),
|
visibility = visibility.serverString(),
|
||||||
|
@ -110,14 +107,20 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
context.startService(sendIntent)
|
context.startService(sendIntent)
|
||||||
|
|
||||||
|
val color = if (BuildConfig.FLAVOR == "green") {
|
||||||
|
Color.parseColor("#19A341")
|
||||||
|
} else {
|
||||||
|
ContextCompat.getColor(context, R.color.tusky_blue)
|
||||||
|
}
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_MENTION + senderIdentifier)
|
val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_MENTION + senderIdentifier)
|
||||||
.setSmallIcon(R.drawable.ic_notify)
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
.setColor(ContextCompat.getColor(context, (R.color.tusky_blue)))
|
.setColor(color)
|
||||||
.setGroup(senderFullName)
|
.setGroup(senderFullName)
|
||||||
.setDefaults(0) // So it doesn't ring twice, notify only in Target callback
|
.setDefaults(0) // So it doesn't ring twice, notify only in Target callback
|
||||||
|
|
||||||
builder.setContentTitle(context.getString(R.string.status_sent))
|
builder.setContentTitle(context.getString(R.string.post_sent))
|
||||||
builder.setContentText(context.getString(R.string.status_sent_long))
|
builder.setContentText(context.getString(R.string.post_sent_long))
|
||||||
|
|
||||||
builder.setSubText(senderFullName)
|
builder.setSubText(senderFullName)
|
||||||
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
|
@ -126,29 +129,6 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
notificationManager.notify(notificationId, builder.build())
|
notificationManager.notify(notificationId, builder.build())
|
||||||
}
|
}
|
||||||
} else if (intent.action == NotificationHelper.COMPOSE_ACTION) {
|
|
||||||
|
|
||||||
context.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
|
|
||||||
|
|
||||||
notificationManager.cancel(notificationId)
|
|
||||||
|
|
||||||
accountManager.setActiveAccount(senderId)
|
|
||||||
|
|
||||||
val composeIntent = ComposeActivity.startIntent(
|
|
||||||
context,
|
|
||||||
ComposeOptions(
|
|
||||||
inReplyToId = citedStatusId,
|
|
||||||
replyVisibility = visibility,
|
|
||||||
contentWarning = spoiler,
|
|
||||||
mentionedUsernames = mentions.toSet(),
|
|
||||||
replyingStatusAuthor = localAuthorId,
|
|
||||||
replyingStatusContent = citedText
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
||||||
|
|
||||||
context.startActivity(composeIntent)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,8 +19,8 @@ import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
||||||
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||||
|
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.entity.NewPoll
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
import com.keylesspalace.tusky.entity.NewStatus
|
import com.keylesspalace.tusky.entity.NewStatus
|
||||||
|
@ -30,18 +30,17 @@ import dagger.android.AndroidInjection
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import java.util.Timer
|
|
||||||
import java.util.TimerTask
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SendTootService : Service(), Injectable {
|
class SendStatusService : Service(), Injectable {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var mastodonApi: MastodonApi
|
lateinit var mastodonApi: MastodonApi
|
||||||
|
@ -50,18 +49,14 @@ class SendTootService : Service(), Injectable {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var eventHub: EventHub
|
lateinit var eventHub: EventHub
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var database: AppDatabase
|
|
||||||
@Inject
|
|
||||||
lateinit var draftHelper: DraftHelper
|
lateinit var draftHelper: DraftHelper
|
||||||
|
|
||||||
private val supervisorJob = SupervisorJob()
|
private val supervisorJob = SupervisorJob()
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.Main + supervisorJob)
|
private val serviceScope = CoroutineScope(Dispatchers.Main + supervisorJob)
|
||||||
|
|
||||||
private val tootsToSend = ConcurrentHashMap<Int, TootToSend>()
|
private val statusesToSend = ConcurrentHashMap<Int, StatusToSend>()
|
||||||
private val sendCalls = ConcurrentHashMap<Int, Call<Status>>()
|
private val sendCalls = ConcurrentHashMap<Int, Call<Status>>()
|
||||||
|
|
||||||
private val timer = Timer()
|
|
||||||
|
|
||||||
private val notificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
|
private val notificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
|
@ -75,38 +70,38 @@ class SendTootService : Service(), Injectable {
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
|
|
||||||
if (intent.hasExtra(KEY_TOOT)) {
|
if (intent.hasExtra(KEY_STATUS)) {
|
||||||
val tootToSend = intent.getParcelableExtra<TootToSend>(KEY_TOOT)
|
val statusToSend = intent.getParcelableExtra<StatusToSend>(KEY_STATUS)
|
||||||
?: throw IllegalStateException("SendTootService started without $KEY_TOOT extra")
|
?: throw IllegalStateException("SendStatusService started without $KEY_STATUS extra")
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val channel = NotificationChannel(CHANNEL_ID, getString(R.string.send_toot_notification_channel_name), NotificationManager.IMPORTANCE_LOW)
|
val channel = NotificationChannel(CHANNEL_ID, getString(R.string.send_post_notification_channel_name), NotificationManager.IMPORTANCE_LOW)
|
||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
var notificationText = tootToSend.warningText
|
var notificationText = statusToSend.warningText
|
||||||
if (notificationText.isBlank()) {
|
if (notificationText.isBlank()) {
|
||||||
notificationText = tootToSend.text
|
notificationText = statusToSend.text
|
||||||
}
|
}
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
|
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
.setSmallIcon(R.drawable.ic_notify)
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
.setContentTitle(getString(R.string.send_toot_notification_title))
|
.setContentTitle(getString(R.string.send_post_notification_title))
|
||||||
.setContentText(notificationText)
|
.setContentText(notificationText)
|
||||||
.setProgress(1, 0, true)
|
.setProgress(1, 0, true)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setColor(ContextCompat.getColor(this, R.color.tusky_blue))
|
.setColor(ContextCompat.getColor(this, R.color.notification_color))
|
||||||
.addAction(0, getString(android.R.string.cancel), cancelSendingIntent(sendingNotificationId))
|
.addAction(0, getString(android.R.string.cancel), cancelSendingIntent(sendingNotificationId))
|
||||||
|
|
||||||
if (tootsToSend.size == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (statusesToSend.size == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
|
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
|
||||||
startForeground(sendingNotificationId, builder.build())
|
startForeground(sendingNotificationId, builder.build())
|
||||||
} else {
|
} else {
|
||||||
notificationManager.notify(sendingNotificationId, builder.build())
|
notificationManager.notify(sendingNotificationId, builder.build())
|
||||||
}
|
}
|
||||||
|
|
||||||
tootsToSend[sendingNotificationId] = tootToSend
|
statusesToSend[sendingNotificationId] = statusToSend
|
||||||
sendToot(sendingNotificationId--)
|
sendStatus(sendingNotificationId--)
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
if (intent.hasExtra(KEY_CANCEL)) {
|
if (intent.hasExtra(KEY_CANCEL)) {
|
||||||
|
@ -117,96 +112,96 @@ class SendTootService : Service(), Injectable {
|
||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendToot(tootId: Int) {
|
private fun sendStatus(statusId: Int) {
|
||||||
|
|
||||||
// when tootToSend == null, sending has been canceled
|
// when statusToSend == null, sending has been canceled
|
||||||
val tootToSend = tootsToSend[tootId] ?: return
|
val statusToSend = statusesToSend[statusId] ?: return
|
||||||
|
|
||||||
// when account == null, user has logged out, cancel sending
|
// when account == null, user has logged out, cancel sending
|
||||||
val account = accountManager.getAccountById(tootToSend.accountId)
|
val account = accountManager.getAccountById(statusToSend.accountId)
|
||||||
|
|
||||||
if (account == null) {
|
if (account == null) {
|
||||||
tootsToSend.remove(tootId)
|
statusesToSend.remove(statusId)
|
||||||
notificationManager.cancel(tootId)
|
notificationManager.cancel(statusId)
|
||||||
stopSelfWhenDone()
|
stopSelfWhenDone()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tootToSend.retries++
|
statusToSend.retries++
|
||||||
|
|
||||||
val newStatus = NewStatus(
|
val newStatus = NewStatus(
|
||||||
tootToSend.text,
|
statusToSend.text,
|
||||||
tootToSend.warningText,
|
statusToSend.warningText,
|
||||||
tootToSend.inReplyToId,
|
statusToSend.inReplyToId,
|
||||||
tootToSend.visibility,
|
statusToSend.visibility,
|
||||||
tootToSend.sensitive,
|
statusToSend.sensitive,
|
||||||
tootToSend.mediaIds,
|
statusToSend.mediaIds,
|
||||||
tootToSend.scheduledAt,
|
statusToSend.scheduledAt,
|
||||||
tootToSend.poll,
|
statusToSend.poll,
|
||||||
tootToSend.quoteId,
|
statusToSend.quoteId,
|
||||||
)
|
)
|
||||||
|
|
||||||
val sendCall = mastodonApi.createStatus(
|
val sendCall = mastodonApi.createStatus(
|
||||||
"Bearer " + account.accessToken,
|
"Bearer " + account.accessToken,
|
||||||
account.domain,
|
account.domain,
|
||||||
tootToSend.idempotencyKey,
|
statusToSend.idempotencyKey,
|
||||||
newStatus
|
newStatus
|
||||||
)
|
)
|
||||||
|
|
||||||
sendCalls[tootId] = sendCall
|
sendCalls[statusId] = sendCall
|
||||||
|
|
||||||
val callback = object : Callback<Status> {
|
val callback = object : Callback<Status> {
|
||||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
||||||
|
serviceScope.launch {
|
||||||
|
|
||||||
val scheduled = !tootToSend.scheduledAt.isNullOrEmpty()
|
val scheduled = !statusToSend.scheduledAt.isNullOrEmpty()
|
||||||
tootsToSend.remove(tootId)
|
statusesToSend.remove(statusId)
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
// If the status was loaded from a draft, delete the draft and associated media files.
|
// If the status was loaded from a draft, delete the draft and associated media files.
|
||||||
if (tootToSend.draftId != 0) {
|
if (statusToSend.draftId != 0) {
|
||||||
serviceScope.launch {
|
draftHelper.deleteDraftAndAttachments(statusToSend.draftId)
|
||||||
draftHelper.deleteDraftAndAttachments(tootToSend.draftId)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (scheduled) {
|
if (scheduled) {
|
||||||
response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch)
|
response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch)
|
||||||
|
} else {
|
||||||
|
response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.cancel(statusId)
|
||||||
} else {
|
} else {
|
||||||
response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch)
|
// the server refused to accept the status, save status & show error message
|
||||||
|
saveStatusToDrafts(statusToSend)
|
||||||
|
|
||||||
|
val builder = NotificationCompat.Builder(this@SendStatusService, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
|
.setContentTitle(getString(R.string.send_post_notification_error_title))
|
||||||
|
.setContentText(getString(R.string.send_post_notification_saved_content))
|
||||||
|
.setColor(
|
||||||
|
ContextCompat.getColor(
|
||||||
|
this@SendStatusService,
|
||||||
|
R.color.notification_color
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
notificationManager.cancel(statusId)
|
||||||
|
notificationManager.notify(errorNotificationId--, builder.build())
|
||||||
}
|
}
|
||||||
|
stopSelfWhenDone()
|
||||||
notificationManager.cancel(tootId)
|
|
||||||
} else {
|
|
||||||
// the server refused to accept the toot, save toot & show error message
|
|
||||||
saveTootToDrafts(tootToSend)
|
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(this@SendTootService, CHANNEL_ID)
|
|
||||||
.setSmallIcon(R.drawable.ic_notify)
|
|
||||||
.setContentTitle(getString(R.string.send_toot_notification_error_title))
|
|
||||||
.setContentText(getString(R.string.send_toot_notification_saved_content))
|
|
||||||
.setColor(ContextCompat.getColor(this@SendTootService, R.color.tusky_blue))
|
|
||||||
|
|
||||||
notificationManager.cancel(tootId)
|
|
||||||
notificationManager.notify(errorNotificationId--, builder.build())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stopSelfWhenDone()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call<Status>, t: Throwable) {
|
override fun onFailure(call: Call<Status>, t: Throwable) {
|
||||||
var backoff = TimeUnit.SECONDS.toMillis(tootToSend.retries.toLong())
|
serviceScope.launch {
|
||||||
if (backoff > MAX_RETRY_INTERVAL) {
|
var backoff = TimeUnit.SECONDS.toMillis(statusToSend.retries.toLong())
|
||||||
backoff = MAX_RETRY_INTERVAL
|
if (backoff > MAX_RETRY_INTERVAL) {
|
||||||
}
|
backoff = MAX_RETRY_INTERVAL
|
||||||
|
}
|
||||||
|
|
||||||
timer.schedule(
|
delay(backoff)
|
||||||
object : TimerTask() {
|
sendStatus(statusId)
|
||||||
override fun run() {
|
}
|
||||||
sendToot(tootId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
backoff
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,65 +210,52 @@ class SendTootService : Service(), Injectable {
|
||||||
|
|
||||||
private fun stopSelfWhenDone() {
|
private fun stopSelfWhenDone() {
|
||||||
|
|
||||||
if (tootsToSend.isEmpty()) {
|
if (statusesToSend.isEmpty()) {
|
||||||
ServiceCompat.stopForeground(this@SendTootService, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
ServiceCompat.stopForeground(this@SendStatusService, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelSending(tootId: Int) {
|
private fun cancelSending(statusId: Int) = serviceScope.launch {
|
||||||
val tootToCancel = tootsToSend.remove(tootId)
|
val statusToCancel = statusesToSend.remove(statusId)
|
||||||
if (tootToCancel != null) {
|
if (statusToCancel != null) {
|
||||||
val sendCall = sendCalls.remove(tootId)
|
val sendCall = sendCalls.remove(statusId)
|
||||||
sendCall?.cancel()
|
sendCall?.cancel()
|
||||||
|
|
||||||
saveTootToDrafts(tootToCancel)
|
saveStatusToDrafts(statusToCancel)
|
||||||
|
|
||||||
val builder = NotificationCompat.Builder(this@SendTootService, CHANNEL_ID)
|
val builder = NotificationCompat.Builder(this@SendStatusService, CHANNEL_ID)
|
||||||
.setSmallIcon(R.drawable.ic_notify)
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
.setContentTitle(getString(R.string.send_toot_notification_cancel_title))
|
.setContentTitle(getString(R.string.send_post_notification_cancel_title))
|
||||||
.setContentText(getString(R.string.send_toot_notification_saved_content))
|
.setContentText(getString(R.string.send_post_notification_saved_content))
|
||||||
.setColor(ContextCompat.getColor(this@SendTootService, R.color.tusky_blue))
|
.setColor(ContextCompat.getColor(this@SendStatusService, R.color.notification_color))
|
||||||
|
|
||||||
notificationManager.notify(tootId, builder.build())
|
notificationManager.notify(statusId, builder.build())
|
||||||
|
|
||||||
timer.schedule(
|
delay(5000)
|
||||||
object : TimerTask() {
|
|
||||||
override fun run() {
|
|
||||||
notificationManager.cancel(tootId)
|
|
||||||
stopSelfWhenDone()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
5000
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveTootToDrafts(toot: TootToSend) {
|
private suspend fun saveStatusToDrafts(status: StatusToSend) {
|
||||||
serviceScope.launch {
|
draftHelper.saveDraft(
|
||||||
draftHelper.saveDraft(
|
draftId = status.draftId,
|
||||||
draftId = toot.draftId,
|
accountId = status.accountId,
|
||||||
accountId = toot.accountId,
|
inReplyToId = status.inReplyToId,
|
||||||
inReplyToId = toot.inReplyToId,
|
content = status.text,
|
||||||
content = toot.text,
|
contentWarning = status.warningText,
|
||||||
contentWarning = toot.warningText,
|
sensitive = status.sensitive,
|
||||||
sensitive = toot.sensitive,
|
visibility = Status.Visibility.byString(status.visibility),
|
||||||
visibility = Status.Visibility.byString(toot.visibility),
|
mediaUris = status.mediaUris,
|
||||||
mediaUris = toot.mediaUris,
|
mediaDescriptions = status.mediaDescriptions,
|
||||||
mediaDescriptions = toot.mediaDescriptions,
|
poll = status.poll,
|
||||||
poll = toot.poll,
|
failedToSend = true
|
||||||
failedToSend = true
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelSendingIntent(tootId: Int): PendingIntent {
|
private fun cancelSendingIntent(statusId: Int): PendingIntent {
|
||||||
|
val intent = Intent(this, SendStatusService::class.java)
|
||||||
val intent = Intent(this, SendTootService::class.java)
|
intent.putExtra(KEY_CANCEL, statusId)
|
||||||
|
return PendingIntent.getService(this, statusId, intent, NotificationHelper.pendingIntentFlags(false))
|
||||||
intent.putExtra(KEY_CANCEL, tootId)
|
|
||||||
|
|
||||||
return PendingIntent.getService(this, tootId, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
@ -283,7 +265,7 @@ class SendTootService : Service(), Injectable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
private const val KEY_TOOT = "toot"
|
private const val KEY_STATUS = "status"
|
||||||
private const val KEY_CANCEL = "cancel_id"
|
private const val KEY_CANCEL = "cancel_id"
|
||||||
private const val CHANNEL_ID = "send_toots"
|
private const val CHANNEL_ID = "send_toots"
|
||||||
|
|
||||||
|
@ -293,21 +275,21 @@ class SendTootService : Service(), Injectable {
|
||||||
private var errorNotificationId = Int.MIN_VALUE // use even more negative ids to not clash with other notis
|
private var errorNotificationId = Int.MIN_VALUE // use even more negative ids to not clash with other notis
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun sendTootIntent(
|
fun sendStatusIntent(
|
||||||
context: Context,
|
context: Context,
|
||||||
tootToSend: TootToSend
|
statusToSend: StatusToSend
|
||||||
): Intent {
|
): Intent {
|
||||||
val intent = Intent(context, SendTootService::class.java)
|
val intent = Intent(context, SendStatusService::class.java)
|
||||||
intent.putExtra(KEY_TOOT, tootToSend)
|
intent.putExtra(KEY_STATUS, statusToSend)
|
||||||
|
|
||||||
if (tootToSend.mediaUris.isNotEmpty()) {
|
if (statusToSend.mediaUris.isNotEmpty()) {
|
||||||
// forward uri permissions
|
// forward uri permissions
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
val uriClip = ClipData(
|
val uriClip = ClipData(
|
||||||
ClipDescription("Toot Media", arrayOf("image/*", "video/*")),
|
ClipDescription("Status Media", arrayOf("image/*", "video/*")),
|
||||||
ClipData.Item(tootToSend.mediaUris[0])
|
ClipData.Item(statusToSend.mediaUris[0])
|
||||||
)
|
)
|
||||||
tootToSend.mediaUris
|
statusToSend.mediaUris
|
||||||
.drop(1)
|
.drop(1)
|
||||||
.forEach { mediaUri ->
|
.forEach { mediaUri ->
|
||||||
uriClip.addItem(ClipData.Item(mediaUri))
|
uriClip.addItem(ClipData.Item(mediaUri))
|
||||||
|
@ -322,7 +304,7 @@ class SendTootService : Service(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class TootToSend(
|
data class StatusToSend(
|
||||||
val text: String,
|
val text: String,
|
||||||
val warningText: String,
|
val warningText: String,
|
||||||
val visibility: String,
|
val visibility: String,
|
|
@ -20,8 +20,8 @@ import androidx.core.content.ContextCompat
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ServiceClient @Inject constructor(private val context: Context) {
|
class ServiceClient @Inject constructor(private val context: Context) {
|
||||||
fun sendToot(tootToSend: TootToSend) {
|
fun sendToot(tootToSend: StatusToSend) {
|
||||||
val intent = SendTootService.sendTootIntent(context, tootToSend)
|
val intent = SendStatusService.sendStatusIntent(context, tootToSend)
|
||||||
ContextCompat.startForegroundService(context, intent)
|
ContextCompat.startForegroundService(context, intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -280,7 +280,7 @@ class EmojiCompatFont(
|
||||||
R.string.caption_blobmoji,
|
R.string.caption_blobmoji,
|
||||||
R.drawable.ic_blobmoji,
|
R.drawable.ic_blobmoji,
|
||||||
"https://tusky.app/hosted/emoji/BlobmojiCompat.ttf",
|
"https://tusky.app/hosted/emoji/BlobmojiCompat.ttf",
|
||||||
"12.0.0"
|
"14.0.1"
|
||||||
)
|
)
|
||||||
val TWEMOJI = EmojiCompatFont(
|
val TWEMOJI = EmojiCompatFont(
|
||||||
"Twemoji",
|
"Twemoji",
|
||||||
|
@ -288,7 +288,7 @@ class EmojiCompatFont(
|
||||||
R.string.caption_twemoji,
|
R.string.caption_twemoji,
|
||||||
R.drawable.ic_twemoji,
|
R.drawable.ic_twemoji,
|
||||||
"https://tusky.app/hosted/emoji/TwemojiCompat.ttf",
|
"https://tusky.app/hosted/emoji/TwemojiCompat.ttf",
|
||||||
"12.0.0"
|
"14.0.0"
|
||||||
)
|
)
|
||||||
val NOTOEMOJI = EmojiCompatFont(
|
val NOTOEMOJI = EmojiCompatFont(
|
||||||
"NotoEmoji",
|
"NotoEmoji",
|
||||||
|
@ -296,7 +296,7 @@ class EmojiCompatFont(
|
||||||
R.string.caption_notoemoji,
|
R.string.caption_notoemoji,
|
||||||
R.drawable.ic_notoemoji,
|
R.drawable.ic_notoemoji,
|
||||||
"https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf",
|
"https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf",
|
||||||
"11.0.0"
|
"14.0.0"
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -149,7 +149,7 @@ fun setClickableMentions(view: TextView, mentions: List<Mention>?, listener: Lin
|
||||||
|
|
||||||
for (mention in mentions) {
|
for (mention in mentions) {
|
||||||
val customSpan = getCustomSpanForMentionUrl(mention.url, mention.id, listener)
|
val customSpan = getCustomSpanForMentionUrl(mention.url, mention.id, listener)
|
||||||
end += 1 + mention.username.length // length of @ + username
|
end += 1 + mention.localUsername.length // length of @ + username
|
||||||
flags = getSpanFlags(customSpan)
|
flags = getSpanFlags(customSpan)
|
||||||
if (firstMention) {
|
if (firstMention) {
|
||||||
firstMention = false
|
firstMention = false
|
||||||
|
@ -160,7 +160,7 @@ fun setClickableMentions(view: TextView, mentions: List<Mention>?, listener: Lin
|
||||||
}
|
}
|
||||||
|
|
||||||
append("@")
|
append("@")
|
||||||
append(mention.username)
|
append(mention.localUsername)
|
||||||
setSpan(customSpan, start, end, flags)
|
setSpan(customSpan, start, end, flags)
|
||||||
append("\u200B") // same reasoning as in setClickableText
|
append("\u200B") // same reasoning as in setClickableText
|
||||||
end += 1 // shift position to take the previous character into account
|
end += 1 // shift position to take the previous character into account
|
||||||
|
|
|
@ -270,12 +270,12 @@ class ListStatusAccessibilityDelegate(
|
||||||
|
|
||||||
private val collapseCwAction = AccessibilityActionCompat(
|
private val collapseCwAction = AccessibilityActionCompat(
|
||||||
R.id.action_collapse_cw,
|
R.id.action_collapse_cw,
|
||||||
context.getString(R.string.status_content_warning_show_less)
|
context.getString(R.string.post_content_warning_show_less)
|
||||||
)
|
)
|
||||||
|
|
||||||
private val expandCwAction = AccessibilityActionCompat(
|
private val expandCwAction = AccessibilityActionCompat(
|
||||||
R.id.action_expand_cw,
|
R.id.action_expand_cw,
|
||||||
context.getString(R.string.status_content_warning_show_more)
|
context.getString(R.string.post_content_warning_show_more)
|
||||||
)
|
)
|
||||||
|
|
||||||
private val replyAction = AccessibilityActionCompat(
|
private val replyAction = AccessibilityActionCompat(
|
||||||
|
|
|
@ -176,9 +176,9 @@ class StatusViewHelper(private val itemView: View) {
|
||||||
sensitiveMediaShow.visibility = View.GONE
|
sensitiveMediaShow.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
sensitiveMediaWarning.text = if (sensitive) {
|
sensitiveMediaWarning.text = if (sensitive) {
|
||||||
context.getString(R.string.status_sensitive_media_title)
|
context.getString(R.string.post_sensitive_media_title)
|
||||||
} else {
|
} else {
|
||||||
context.getString(R.string.status_media_hidden_title)
|
context.getString(R.string.post_media_hidden_title)
|
||||||
}
|
}
|
||||||
|
|
||||||
sensitiveMediaWarning.visibility = if (showingContent) View.GONE else View.VISIBLE
|
sensitiveMediaWarning.visibility = if (showingContent) View.GONE else View.VISIBLE
|
||||||
|
@ -225,7 +225,7 @@ class StatusViewHelper(private val itemView: View) {
|
||||||
val context = mediaLabel.context
|
val context = mediaLabel.context
|
||||||
var labelText = getLabelTypeText(context, attachments[0].type)
|
var labelText = getLabelTypeText(context, attachments[0].type)
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
val sensitiveText = context.getString(R.string.status_sensitive_media_title)
|
val sensitiveText = context.getString(R.string.post_sensitive_media_title)
|
||||||
labelText += String.format(" (%s)", sensitiveText)
|
labelText += String.format(" (%s)", sensitiveText)
|
||||||
}
|
}
|
||||||
mediaLabel.text = labelText
|
mediaLabel.text = labelText
|
||||||
|
@ -239,10 +239,10 @@ class StatusViewHelper(private val itemView: View) {
|
||||||
|
|
||||||
private fun getLabelTypeText(context: Context, type: Attachment.Type): String {
|
private fun getLabelTypeText(context: Context, type: Attachment.Type): String {
|
||||||
return when (type) {
|
return when (type) {
|
||||||
Attachment.Type.IMAGE -> context.getString(R.string.status_media_images)
|
Attachment.Type.IMAGE -> context.getString(R.string.post_media_images)
|
||||||
Attachment.Type.GIFV, Attachment.Type.VIDEO -> context.getString(R.string.status_media_video)
|
Attachment.Type.GIFV, Attachment.Type.VIDEO -> context.getString(R.string.post_media_video)
|
||||||
Attachment.Type.AUDIO -> context.getString(R.string.status_media_audio)
|
Attachment.Type.AUDIO -> context.getString(R.string.post_media_audio)
|
||||||
else -> context.getString(R.string.status_media_attachments)
|
else -> context.getString(R.string.post_media_attachments)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,51 +16,6 @@ fun randomAlphanumericString(count: Int): String {
|
||||||
return String(chars)
|
return String(chars)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We sort statuses by ID. Something we need to invent some ID for placeholder.
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Increment" string so that during sorting it's bigger than [this]. Inverse operation to [dec].
|
|
||||||
*/
|
|
||||||
fun String.inc(): String {
|
|
||||||
val builder = this.toCharArray()
|
|
||||||
var i = builder.lastIndex
|
|
||||||
|
|
||||||
while (i >= 0) {
|
|
||||||
if (builder[i] < 'z') {
|
|
||||||
builder[i] = builder[i].inc()
|
|
||||||
return String(builder)
|
|
||||||
} else {
|
|
||||||
builder[i] = '0'
|
|
||||||
}
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
return String(
|
|
||||||
CharArray(builder.size + 1) { index ->
|
|
||||||
if (index == 0) '0' else builder[index - 1]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* "Decrement" string so that during sorting it's smaller than [this]. Inverse operation to [inc].
|
|
||||||
*/
|
|
||||||
fun String.dec(): String {
|
|
||||||
if (this.isEmpty()) return this
|
|
||||||
|
|
||||||
val builder = this.toCharArray()
|
|
||||||
var i = builder.lastIndex
|
|
||||||
while (i >= 0) {
|
|
||||||
if (builder[i] > '0') {
|
|
||||||
builder[i] = builder[i].dec()
|
|
||||||
return String(builder)
|
|
||||||
} else {
|
|
||||||
builder[i] = 'z'
|
|
||||||
}
|
|
||||||
i--
|
|
||||||
}
|
|
||||||
return String(builder.copyOfRange(1, builder.size))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A < B (strictly) by length and then by content.
|
* A < B (strictly) by length and then by content.
|
||||||
* Examples:
|
* Examples:
|
||||||
|
@ -78,6 +33,19 @@ fun String.isLessThan(other: String): Boolean {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A <= B (strictly) by length and then by content.
|
||||||
|
* Examples:
|
||||||
|
* "abc" <= "bcd"
|
||||||
|
* "ab" <= "abc"
|
||||||
|
* "cb" <= "abc"
|
||||||
|
* "ab" <= "ab"
|
||||||
|
* not: "abc" > "cb"
|
||||||
|
*/
|
||||||
|
fun String.isLessThanOrEqual(other: String): Boolean {
|
||||||
|
return this == other || isLessThan(other)
|
||||||
|
}
|
||||||
|
|
||||||
fun Spanned.trimTrailingWhitespace(): Spanned {
|
fun Spanned.trimTrailingWhitespace(): Spanned {
|
||||||
var i = length
|
var i = length
|
||||||
do {
|
do {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
import com.keylesspalace.tusky.entity.Account;
|
||||||
import com.keylesspalace.tusky.entity.Notification;
|
import com.keylesspalace.tusky.entity.Notification;
|
||||||
|
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@ -44,11 +45,11 @@ public abstract class NotificationViewData {
|
||||||
public static final class Concrete extends NotificationViewData {
|
public static final class Concrete extends NotificationViewData {
|
||||||
private final Notification.Type type;
|
private final Notification.Type type;
|
||||||
private final String id;
|
private final String id;
|
||||||
private final Account account;
|
private final TimelineAccount account;
|
||||||
@Nullable
|
@Nullable
|
||||||
private final StatusViewData.Concrete statusViewData;
|
private final StatusViewData.Concrete statusViewData;
|
||||||
|
|
||||||
public Concrete(Notification.Type type, String id, Account account,
|
public Concrete(Notification.Type type, String id, TimelineAccount account,
|
||||||
@Nullable StatusViewData.Concrete statusViewData) {
|
@Nullable StatusViewData.Concrete statusViewData) {
|
||||||
this.type = type;
|
this.type = type;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
|
@ -64,7 +65,7 @@ public abstract class NotificationViewData {
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Account getAccount() {
|
public TimelineAccount getAccount() {
|
||||||
return account;
|
return account;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,12 +22,11 @@ import com.keylesspalace.tusky.entity.Status
|
||||||
/**
|
/**
|
||||||
* Created by charlag on 11/07/2017.
|
* Created by charlag on 11/07/2017.
|
||||||
*
|
*
|
||||||
*
|
|
||||||
* Class to represent data required to display either a notification or a placeholder.
|
* Class to represent data required to display either a notification or a placeholder.
|
||||||
* It is either a [StatusViewData.Concrete] or a [StatusViewData.Placeholder].
|
* It is either a [StatusViewData.Concrete] or a [StatusViewData.Placeholder].
|
||||||
*/
|
*/
|
||||||
sealed class StatusViewData private constructor() {
|
sealed class StatusViewData {
|
||||||
abstract val viewDataId: Long
|
abstract val id: String
|
||||||
|
|
||||||
data class Concrete(
|
data class Concrete(
|
||||||
val status: Status,
|
val status: Status,
|
||||||
|
@ -49,8 +48,8 @@ sealed class StatusViewData private constructor() {
|
||||||
/** Whether the status meets the requirement to be collapse */
|
/** Whether the status meets the requirement to be collapse */
|
||||||
val isCollapsed: Boolean,
|
val isCollapsed: Boolean,
|
||||||
) : StatusViewData() {
|
) : StatusViewData() {
|
||||||
override val viewDataId: Long
|
override val id: String
|
||||||
get() = status.id.hashCode().toLong()
|
get() = status.id
|
||||||
|
|
||||||
val content: Spanned
|
val content: Spanned
|
||||||
val spoilerText: String
|
val spoilerText: String
|
||||||
|
@ -116,9 +115,6 @@ sealed class StatusViewData private constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val id: String
|
|
||||||
get() = status.id
|
|
||||||
|
|
||||||
/** Helper for Java */
|
/** Helper for Java */
|
||||||
fun copyWithStatus(status: Status): Concrete {
|
fun copyWithStatus(status: Status): Concrete {
|
||||||
return copy(status = status)
|
return copy(status = status)
|
||||||
|
@ -140,10 +136,10 @@ sealed class StatusViewData private constructor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class Placeholder(val id: String, val isLoading: Boolean) : StatusViewData() {
|
data class Placeholder(
|
||||||
override val viewDataId: Long
|
override val id: String,
|
||||||
get() = id.hashCode().toLong()
|
val isLoading: Boolean
|
||||||
}
|
) : StatusViewData()
|
||||||
|
|
||||||
fun asStatusOrNull() = this as? Concrete
|
fun asStatusOrNull() = this as? Concrete
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
package com.keylesspalace.tusky.viewmodel
|
package com.keylesspalace.tusky.viewmodel
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.Either
|
import com.keylesspalace.tusky.util.Either
|
||||||
import com.keylesspalace.tusky.util.Either.Left
|
import com.keylesspalace.tusky.util.Either.Left
|
||||||
|
@ -28,7 +28,7 @@ import io.reactivex.rxjava3.core.Observable
|
||||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
data class State(val accounts: Either<Throwable, List<Account>>, val searchResult: List<Account>?)
|
data class State(val accounts: Either<Throwable, List<TimelineAccount>>, val searchResult: List<TimelineAccount>?)
|
||||||
|
|
||||||
class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) : RxAwareViewModel() {
|
class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) : RxAwareViewModel() {
|
||||||
|
|
||||||
|
@ -49,7 +49,7 @@ class AccountsInListViewModel @Inject constructor(private val api: MastodonApi)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addAccountToList(listId: String, account: Account) {
|
fun addAccountToList(listId: String, account: TimelineAccount) {
|
||||||
api.addCountToList(listId, listOf(account.id))
|
api.addCountToList(listId, listOf(account.id))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{
|
{
|
||||||
|
|
|
@ -63,7 +63,7 @@ class QuickTootViewModel @Inject constructor(
|
||||||
|
|
||||||
fun composeOptions(tootRightNow: Boolean): ComposeActivity.ComposeOptions {
|
fun composeOptions(tootRightNow: Boolean): ComposeActivity.ComposeOptions {
|
||||||
return ComposeActivity.ComposeOptions(
|
return ComposeActivity.ComposeOptions(
|
||||||
tootText = content.value,
|
content = content.value,
|
||||||
mentionedUsernames = inReplyTo.value
|
mentionedUsernames = inReplyTo.value
|
||||||
?.let {
|
?.let {
|
||||||
linkedSetOf(it.account.username, *(it.mentions.map { mention -> mention.username }.toTypedArray()))
|
linkedSetOf(it.account.username, *(it.mentions.map { mention -> mention.username }.toTypedArray()))
|
||||||
|
|
|
@ -10,10 +10,10 @@ import androidx.annotation.Px;
|
||||||
|
|
||||||
import com.google.android.material.button.MaterialButton;
|
import com.google.android.material.button.MaterialButton;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
|
||||||
import com.keylesspalace.tusky.entity.Emoji;
|
import com.keylesspalace.tusky.entity.Emoji;
|
||||||
import com.keylesspalace.tusky.entity.HashTag;
|
import com.keylesspalace.tusky.entity.HashTag;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||||
|
@ -23,21 +23,21 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class QuoteInlineHelper {
|
public class QuoteInlineHelper {
|
||||||
private Status quoteStatus;
|
private final Status quoteStatus;
|
||||||
|
|
||||||
private View quoteContainer;
|
private final View quoteContainer;
|
||||||
private ImageView quoteAvatar;
|
private final ImageView quoteAvatar;
|
||||||
private TextView quoteDisplayName;
|
private final TextView quoteDisplayName;
|
||||||
private TextView quoteUsername;
|
private final TextView quoteUsername;
|
||||||
private TextView quoteContentWarningDescription;
|
private final TextView quoteContentWarningDescription;
|
||||||
private MaterialButton quoteContentWarningButton;
|
private final MaterialButton quoteContentWarningButton;
|
||||||
private TextView quoteContent;
|
private final TextView quoteContent;
|
||||||
private TextView quoteMedia;
|
private final TextView quoteMedia;
|
||||||
|
|
||||||
private LinkListener listener;
|
private final LinkListener listener;
|
||||||
@Px
|
@Px
|
||||||
private int avatarRadius24dp;
|
private final int avatarRadius24dp;
|
||||||
private StatusDisplayOptions statusDisplayOptions;
|
private final StatusDisplayOptions statusDisplayOptions;
|
||||||
|
|
||||||
public QuoteInlineHelper(Status status, View container, LinkListener listener,
|
public QuoteInlineHelper(Status status, View container, LinkListener listener,
|
||||||
@Px int avatarRadius24dp, StatusDisplayOptions statusDisplayOptions) {
|
@Px int avatarRadius24dp, StatusDisplayOptions statusDisplayOptions) {
|
||||||
|
@ -62,7 +62,7 @@ public class QuoteInlineHelper {
|
||||||
|
|
||||||
private void setUsername(String name) {
|
private void setUsername(String name) {
|
||||||
Context context = quoteUsername.getContext();
|
Context context = quoteUsername.getContext();
|
||||||
String format = context.getString(R.string.status_username_format);
|
String format = context.getString(R.string.post_username_format);
|
||||||
String usernameText = String.format(format, name);
|
String usernameText = String.format(format, name);
|
||||||
quoteUsername.setText(usernameText);
|
quoteUsername.setText(usernameText);
|
||||||
}
|
}
|
||||||
|
@ -97,10 +97,10 @@ public class QuoteInlineHelper {
|
||||||
private void setContentVisibility(boolean show) {
|
private void setContentVisibility(boolean show) {
|
||||||
if (show) {
|
if (show) {
|
||||||
quoteContent.setVisibility(View.VISIBLE);
|
quoteContent.setVisibility(View.VISIBLE);
|
||||||
quoteContentWarningButton.setText(R.string.status_content_warning_show_less);
|
quoteContentWarningButton.setText(R.string.post_content_warning_show_less);
|
||||||
} else {
|
} else {
|
||||||
quoteContent.setVisibility(View.GONE);
|
quoteContent.setVisibility(View.GONE);
|
||||||
quoteContentWarningButton.setText(R.string.status_content_warning_show_more);
|
quoteContentWarningButton.setText(R.string.post_content_warning_show_more);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +120,7 @@ public class QuoteInlineHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setupQuoteContainer() {
|
public void setupQuoteContainer() {
|
||||||
Account account = quoteStatus.getAccount();
|
TimelineAccount account = quoteStatus.getAccount();
|
||||||
setDisplayName(account.getName(), account.getEmojis());
|
setDisplayName(account.getName(), account.getEmojis());
|
||||||
setUsername(account.getUsername());
|
setUsername(account.getUsername());
|
||||||
setContent(
|
setContent(
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
Before Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 16 KiB |
Binary file not shown.
Before Width: | Height: | Size: 30 KiB |
Binary file not shown.
Before Width: | Height: | Size: 43 KiB |
|
@ -1,11 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<item>
|
|
||||||
<color android:color="@color/ic_launcher_background"/>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<bitmap
|
|
||||||
android:src="@drawable/splash"
|
|
||||||
android:gravity="center" />
|
|
||||||
</item>
|
|
||||||
</layer-list>
|
|
|
@ -331,7 +331,7 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingBottom="6dp"
|
android:paddingBottom="6dp"
|
||||||
android:text="@string/title_statuses"
|
android:text="@string/title_posts"
|
||||||
android:textColor="@color/account_tab_font_color"
|
android:textColor="@color/account_tab_font_color"
|
||||||
android:textSize="?attr/status_text_medium" />
|
android:textSize="?attr/status_text_medium" />
|
||||||
|
|
||||||
|
|
|
@ -371,10 +371,10 @@
|
||||||
android:layout_width="36dp"
|
android:layout_width="36dp"
|
||||||
android:layout_height="36dp"
|
android:layout_height="36dp"
|
||||||
android:layout_marginEnd="4dp"
|
android:layout_marginEnd="4dp"
|
||||||
android:contentDescription="@string/action_schedule_toot"
|
android:contentDescription="@string/action_schedule_post"
|
||||||
android:padding="4dp"
|
android:padding="4dp"
|
||||||
app:srcCompat="@drawable/ic_access_time"
|
app:srcCompat="@drawable/ic_access_time"
|
||||||
app:tooltipText="@string/action_schedule_toot" />
|
app:tooltipText="@string/action_schedule_post" />
|
||||||
|
|
||||||
<Space
|
<Space
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:gravity="center"
|
android:gravity="center"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
tools:context="com.keylesspalace.tusky.LoginActivity">
|
tools:context="com.keylesspalace.tusky.components.login.LoginActivity">
|
||||||
|
|
||||||
<ScrollView
|
<ScrollView
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context=".components.scheduled.ScheduledTootActivity">
|
tools:context=".components.scheduled.ScheduledStatusActivity">
|
||||||
|
|
||||||
<include
|
<include
|
||||||
android:id="@+id/includedToolbar"
|
android:id="@+id/includedToolbar"
|
|
@ -154,7 +154,7 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
app:layout_constraintTop_toBottomOf="@id/status_content_warning_description"
|
app:layout_constraintTop_toBottomOf="@id/status_content_warning_description"
|
||||||
tools:text="@string/status_content_warning_show_more"
|
tools:text="@string/post_content_warning_show_more"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<androidx.emoji.widget.EmojiTextView
|
<androidx.emoji.widget.EmojiTextView
|
||||||
|
@ -189,7 +189,7 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
app:layout_constraintTop_toBottomOf="@id/status_content"
|
app:layout_constraintTop_toBottomOf="@id/status_content"
|
||||||
tools:text="@string/status_content_show_less"
|
tools:text="@string/post_content_show_less"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
android:drawablePadding="4dp"
|
android:drawablePadding="4dp"
|
||||||
android:fontFamily="sans-serif-medium"
|
android:fontFamily="sans-serif-medium"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:text="@string/drafts_toot_failed_to_send"
|
android:text="@string/drafts_post_failed_to_send"
|
||||||
android:textColor="@color/tusky_red"
|
android:textColor="@color/tusky_red"
|
||||||
android:textSize="?attr/status_text_medium"
|
android:textSize="?attr/status_text_medium"
|
||||||
app:drawableStartCompat="@drawable/ic_alert_circle"
|
app:drawableStartCompat="@drawable/ic_alert_circle"
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
app:layout_constraintEnd_toStartOf="@id/deleteButton"
|
app:layout_constraintEnd_toStartOf="@id/deleteButton"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/contentWarning"
|
app:layout_constraintTop_toBottomOf="@id/contentWarning"
|
||||||
tools:text="Some toot content. May be very long." />
|
tools:text="Some post content. May be very long." />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/deleteButton"
|
android:id="@+id/deleteButton"
|
||||||
|
|
|
@ -46,7 +46,7 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintStart_toStartOf="@id/guideBegin"
|
app:layout_constraintStart_toStartOf="@id/guideBegin"
|
||||||
app:layout_constraintTop_toBottomOf="@id/statusContentWarningDescription"
|
app:layout_constraintTop_toBottomOf="@id/statusContentWarningDescription"
|
||||||
tools:text="@string/status_content_warning_show_more"
|
tools:text="@string/post_content_warning_show_more"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<androidx.emoji.widget.EmojiTextView
|
<androidx.emoji.widget.EmojiTextView
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintStart_toStartOf="@id/guideBegin"
|
app:layout_constraintStart_toStartOf="@id/guideBegin"
|
||||||
app:layout_constraintTop_toBottomOf="@id/statusContent"
|
app:layout_constraintTop_toBottomOf="@id/statusContent"
|
||||||
tools:text="@string/status_content_show_less"
|
tools:text="@string/post_content_show_less"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
|
|
@ -133,13 +133,13 @@
|
||||||
android:paddingRight="16dp"
|
android:paddingRight="16dp"
|
||||||
android:paddingBottom="4dp"
|
android:paddingBottom="4dp"
|
||||||
android:textAllCaps="true"
|
android:textAllCaps="true"
|
||||||
android:textOff="@string/status_content_warning_show_more"
|
android:textOff="@string/post_content_warning_show_more"
|
||||||
android:textOn="@string/status_content_warning_show_less"
|
android:textOn="@string/post_content_warning_show_less"
|
||||||
android:textSize="?attr/status_text_medium"
|
android:textSize="?attr/status_text_medium"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
app:layout_constraintTop_toBottomOf="@id/status_content_warning_description"
|
app:layout_constraintTop_toBottomOf="@id/status_content_warning_description"
|
||||||
tools:text="@string/status_content_warning_show_more"
|
tools:text="@string/post_content_warning_show_more"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<androidx.emoji.widget.EmojiTextView
|
<androidx.emoji.widget.EmojiTextView
|
||||||
|
@ -244,7 +244,7 @@
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
app:layout_constraintStart_toStartOf="@id/status_display_name"
|
||||||
app:layout_constraintTop_toBottomOf="@id/status_content"
|
app:layout_constraintTop_toBottomOf="@id/status_content"
|
||||||
tools:text="@string/status_content_show_less"
|
tools:text="@string/post_content_show_less"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<include
|
<include
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue