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).
|
||||
|
||||
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
|
||||
|
||||
|
@ -15,14 +15,14 @@ Tusky is a beautiful Android client for [Mastodon](https://github.com/tootsuite/
|
|||
- Most Mastodon APIs implemented
|
||||
- Multi-Account support
|
||||
- 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
|
||||
- Optimized for all screen sizes
|
||||
- Completely open-source - no non-free dependencies like Google services
|
||||
|
||||
### 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/
|
||||
|
||||
|
|
|
@ -15,11 +15,11 @@ def getGitSha = {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 31
|
||||
defaultConfig {
|
||||
applicationId 'net.accelf.yuito'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
targetSdkVersion 31
|
||||
versionCode 43
|
||||
versionName '4.1.3'
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
@ -96,12 +96,12 @@ android {
|
|||
}
|
||||
|
||||
ext.coroutinesVersion = "1.6.0"
|
||||
ext.lifecycleVersion = "2.3.1"
|
||||
ext.roomVersion = '2.3.0'
|
||||
ext.lifecycleVersion = "2.4.1"
|
||||
ext.roomVersion = '2.4.2'
|
||||
ext.retrofitVersion = '2.9.0'
|
||||
ext.okhttpVersion = '4.9.3'
|
||||
ext.glideVersion = '4.12.0'
|
||||
ext.daggerVersion = '2.40.5'
|
||||
ext.glideVersion = '4.13.1'
|
||||
ext.daggerVersion = '2.41'
|
||||
ext.materialdrawerVersion = '8.4.5'
|
||||
|
||||
repositories {
|
||||
|
@ -117,33 +117,35 @@ dependencies {
|
|||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion"
|
||||
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$coroutinesVersion"
|
||||
|
||||
implementation "androidx.core:core-ktx:1.5.0"
|
||||
implementation "androidx.appcompat:appcompat:1.3.0"
|
||||
implementation "androidx.fragment:fragment-ktx:1.3.4"
|
||||
implementation "androidx.browser:browser:1.3.0"
|
||||
implementation "androidx.core:core-ktx:1.7.0"
|
||||
implementation "androidx.appcompat:appcompat:1.4.1"
|
||||
implementation "androidx.fragment:fragment-ktx:1.4.1"
|
||||
implementation "androidx.browser:browser:1.4.0"
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation "androidx.recyclerview:recyclerview:1.2.1"
|
||||
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
||||
implementation "androidx.cardview:cardview:1.0.0"
|
||||
implementation "androidx.preference:preference-ktx:1.1.1"
|
||||
implementation "androidx.sharetarget:sharetarget:1.1.0"
|
||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||
implementation "androidx.sharetarget:sharetarget:1.2.0-rc01"
|
||||
implementation "androidx.emoji:emoji:1.1.0"
|
||||
implementation "androidx.emoji:emoji-appcompat:1.1.0"
|
||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
|
||||
implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.2"
|
||||
implementation "androidx.paging:paging-runtime-ktx:3.0.0"
|
||||
implementation "androidx.constraintlayout:constraintlayout:2.1.3"
|
||||
implementation "androidx.paging:paging-runtime-ktx:3.1.1"
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
implementation "androidx.work:work-runtime:2.5.0"
|
||||
implementation "androidx.work:work-runtime:2.7.1"
|
||||
implementation "androidx.room:room-ktx:$roomVersion"
|
||||
implementation "androidx.room:room-paging:$roomVersion"
|
||||
implementation "androidx.room:room-rxjava3:$roomVersion"
|
||||
kapt "androidx.room:room-compiler:$roomVersion"
|
||||
implementation 'androidx.core:core-splashscreen:1.0.0-beta02'
|
||||
|
||||
implementation "com.google.android.material:material:1.4.0"
|
||||
implementation "com.google.android.material:material:1.5.0"
|
||||
|
||||
implementation "com.google.code.gson:gson:2.8.9"
|
||||
implementation "com.google.code.gson:gson:2.9.0"
|
||||
|
||||
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
|
||||
implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion"
|
||||
|
@ -158,14 +160,14 @@ dependencies {
|
|||
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
|
||||
kapt "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
|
||||
implementation "com.github.penfeizhou.android.animation:glide-plugin:2.17.0"
|
||||
implementation "com.github.penfeizhou.android.animation:glide-plugin:2.20.0"
|
||||
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.0.12"
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.1.3"
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.0"
|
||||
implementation "io.reactivex.rxjava3:rxkotlin:3.0.1"
|
||||
|
||||
implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.0.0"
|
||||
implementation "com.uber.autodispose2:autodispose:2.0.0"
|
||||
implementation "com.uber.autodispose2:autodispose-androidx-lifecycle:2.1.1"
|
||||
implementation "com.uber.autodispose2:autodispose:2.1.1"
|
||||
|
||||
implementation "com.google.dagger:dagger:$daggerVersion"
|
||||
kapt "com.google.dagger:dagger-compiler:$daggerVersion"
|
||||
|
|
|
@ -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:theme="@style/TuskyTheme"
|
||||
android:usesCleartextTraffic="false">
|
||||
|
||||
<activity
|
||||
android:name=".SplashActivity"
|
||||
android:theme="@style/SplashTheme">
|
||||
android:name=".components.login.LoginActivity"
|
||||
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>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</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>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
|
||||
|
@ -98,6 +84,9 @@
|
|||
<meta-data
|
||||
android:name="android.service.chooser.chooser_target_service"
|
||||
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/share_shortcuts" />
|
||||
|
||||
</activity>
|
||||
<activity
|
||||
|
@ -107,7 +96,6 @@
|
|||
<activity
|
||||
android:name=".ViewThreadActivity"
|
||||
android:configChanges="orientation|screenSize" />
|
||||
<activity android:name=".ViewTagActivity" />
|
||||
<activity
|
||||
android:name=".ViewMediaActivity"
|
||||
android:theme="@style/TuskyBaseTheme" />
|
||||
|
@ -125,7 +113,8 @@
|
|||
android:theme="@style/Base.Theme.AppCompat" />
|
||||
<activity
|
||||
android:name=".components.search.SearchActivity"
|
||||
android:launchMode="singleTop">
|
||||
android:launchMode="singleTop"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEARCH" />
|
||||
</intent-filter>
|
||||
|
@ -135,18 +124,18 @@
|
|||
android:resource="@xml/searchable" />
|
||||
</activity>
|
||||
<activity android:name=".ListsActivity" />
|
||||
<activity android:name=".ModalTimelineActivity" />
|
||||
<activity android:name=".LicenseActivity" />
|
||||
<activity android:name=".FiltersActivity" />
|
||||
<activity
|
||||
android:name=".components.report.ReportActivity"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
<activity android:name=".components.instancemute.InstanceListActivity" />
|
||||
<activity android:name=".components.scheduled.ScheduledTootActivity" />
|
||||
<activity android:name=".components.scheduled.ScheduledStatusActivity" />
|
||||
<activity android:name=".components.announcements.AnnouncementsActivity" />
|
||||
<activity android:name=".components.drafts.DraftsActivity" />
|
||||
|
||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver"
|
||||
android:exported="false" />
|
||||
<receiver
|
||||
android:name=".receiver.SendStatusBroadcastReceiver"
|
||||
android:enabled="true"
|
||||
|
@ -155,15 +144,17 @@
|
|||
<service
|
||||
android:name=".service.TuskyTileService"
|
||||
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:exported="true"
|
||||
tools:targetApi="24">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<service android:name=".service.SendTootService" />
|
||||
<service android:name=".service.SendStatusService"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
|
@ -177,10 +168,15 @@
|
|||
|
||||
<!-- disable automatic WorkManager initialization -->
|
||||
<provider
|
||||
android:name="androidx.work.impl.WorkManagerInitializer"
|
||||
android:authorities="${applicationId}.workmanager-init"
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
android:exported="false"
|
||||
tools:node="remove" />
|
||||
tools:node="merge">
|
||||
<meta-data
|
||||
android:name="androidx.work.WorkManagerInitializer"
|
||||
android:value="androidx.startup"
|
||||
tools:node="remove" />
|
||||
</provider>
|
||||
|
||||
<activity android:name="net.accelf.yuito.AccessTokenLoginActivity" />
|
||||
</application>
|
||||
|
|
|
@ -34,7 +34,7 @@ import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding
|
|||
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
import com.keylesspalace.tusky.entity.Account
|
||||
import com.keylesspalace.tusky.entity.TimelineAccount
|
||||
import com.keylesspalace.tusky.settings.PrefKeys
|
||||
import com.keylesspalace.tusky.util.BindingHolder
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
|
@ -49,7 +49,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
private typealias AccountInfo = Pair<Account, Boolean>
|
||||
private typealias AccountInfo = Pair<TimelineAccount, Boolean>
|
||||
|
||||
class AccountsInListFragment : DialogFragment(), Injectable {
|
||||
|
||||
|
@ -168,21 +168,21 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
viewModel.deleteAccountFromList(listId, accountId)
|
||||
}
|
||||
|
||||
private fun onAddToList(account: Account) {
|
||||
private fun onAddToList(account: TimelineAccount) {
|
||||
viewModel.addAccountToList(listId, account)
|
||||
}
|
||||
|
||||
private object AccountDiffer : DiffUtil.ItemCallback<Account>() {
|
||||
override fun areItemsTheSame(oldItem: Account, newItem: Account): Boolean {
|
||||
return oldItem == newItem
|
||||
private object AccountDiffer : DiffUtil.ItemCallback<TimelineAccount>() {
|
||||
override fun areItemsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean {
|
||||
return oldItem.id == newItem.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Account, newItem: Account): Boolean {
|
||||
return oldItem.deepEquals(newItem)
|
||||
override fun areContentsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
inner class Adapter : ListAdapter<Account, BindingHolder<ItemFollowRequestBinding>>(AccountDiffer) {
|
||||
inner class Adapter : ListAdapter<TimelineAccount, BindingHolder<ItemFollowRequestBinding>>(AccountDiffer) {
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ItemFollowRequestBinding> {
|
||||
val binding = ItemFollowRequestBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
|
@ -209,12 +209,11 @@ class AccountsInListFragment : DialogFragment(), Injectable {
|
|||
|
||||
private object SearchDiffer : DiffUtil.ItemCallback<AccountInfo>() {
|
||||
override fun areItemsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
||||
return oldItem == newItem
|
||||
return oldItem.first.id == newItem.first.id
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean {
|
||||
return oldItem.second == newItem.second &&
|
||||
oldItem.first.deepEquals(newItem.first)
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,6 +38,7 @@ import androidx.preference.PreferenceManager;
|
|||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.keylesspalace.tusky.adapter.AccountSelectionAdapter;
|
||||
import com.keylesspalace.tusky.components.login.LoginActivity;
|
||||
import com.keylesspalace.tusky.db.AccountEntity;
|
||||
import com.keylesspalace.tusky.db.AccountManager;
|
||||
import com.keylesspalace.tusky.di.Injectable;
|
||||
|
|
|
@ -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 {
|
||||
|
||||