Merge tag 'v18.0' into develop
This commit is contained in:
commit
474fca8f86
203 changed files with 6626 additions and 3019 deletions
|
@ -7,9 +7,13 @@ apply from: "../instance-build.gradle"
|
||||||
|
|
||||||
def getGitSha = {
|
def getGitSha = {
|
||||||
def stdout = new ByteArrayOutputStream()
|
def stdout = new ByteArrayOutputStream()
|
||||||
exec {
|
try {
|
||||||
commandLine 'git', 'rev-parse', '--short', 'HEAD'
|
exec {
|
||||||
standardOutput = stdout
|
commandLine 'git', 'rev-parse', '--short', 'HEAD'
|
||||||
|
standardOutput = stdout
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
return "unknown"
|
||||||
}
|
}
|
||||||
return stdout.toString().trim()
|
return stdout.toString().trim()
|
||||||
}
|
}
|
||||||
|
@ -21,7 +25,7 @@ android {
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 31
|
targetSdkVersion 31
|
||||||
versionCode 87
|
versionCode 87
|
||||||
versionName "17.0-CW1"
|
versionName "18.0-CW1"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary = true
|
vectorDrawables.useSupportLibrary = true
|
||||||
|
|
||||||
|
@ -89,7 +93,7 @@ android {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.coroutinesVersion = "1.6.0"
|
ext.coroutinesVersion = "1.6.1"
|
||||||
ext.lifecycleVersion = "2.4.1"
|
ext.lifecycleVersion = "2.4.1"
|
||||||
ext.roomVersion = '2.4.2'
|
ext.roomVersion = '2.4.2'
|
||||||
ext.retrofitVersion = '2.9.0'
|
ext.retrofitVersion = '2.9.0'
|
||||||
|
@ -97,11 +101,11 @@ ext.okhttpVersion = '4.9.3'
|
||||||
ext.glideVersion = '4.13.1'
|
ext.glideVersion = '4.13.1'
|
||||||
ext.daggerVersion = '2.41'
|
ext.daggerVersion = '2.41'
|
||||||
ext.materialdrawerVersion = '8.4.5'
|
ext.materialdrawerVersion = '8.4.5'
|
||||||
|
ext.emoji2_version = '1.1.0'
|
||||||
|
ext.filemojicompat_version = '3.2.1'
|
||||||
|
|
||||||
// if libraries are changed here, they should also be changed in LicenseActivity
|
// if libraries are changed here, they should also be changed in LicenseActivity
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
|
@ -115,8 +119,9 @@ dependencies {
|
||||||
implementation "androidx.cardview:cardview:1.0.0"
|
implementation "androidx.cardview:cardview:1.0.0"
|
||||||
implementation "androidx.preference:preference-ktx:1.2.0"
|
implementation "androidx.preference:preference-ktx:1.2.0"
|
||||||
implementation "androidx.sharetarget:sharetarget:1.2.0-rc01"
|
implementation "androidx.sharetarget:sharetarget:1.2.0-rc01"
|
||||||
implementation "androidx.emoji:emoji:1.1.0"
|
implementation "androidx.emoji2:emoji2:$emoji2_version"
|
||||||
implementation "androidx.emoji:emoji-appcompat:1.1.0"
|
implementation "androidx.emoji2:emoji2-views:$emoji2_version"
|
||||||
|
implementation "androidx.emoji2:emoji2-views-helper:$emoji2_version"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion"
|
implementation "androidx.lifecycle:lifecycle-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"
|
||||||
|
@ -127,7 +132,6 @@ dependencies {
|
||||||
implementation "androidx.work:work-runtime:2.7.1"
|
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-paging:$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 'androidx.core:core-splashscreen:1.0.0-beta02'
|
||||||
|
|
||||||
|
@ -138,6 +142,7 @@ dependencies {
|
||||||
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"
|
||||||
implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion"
|
implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion"
|
||||||
|
implementation "at.connyduck:kotlin-result-calladapter:1.0.1"
|
||||||
|
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||||
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
|
implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion"
|
||||||
|
@ -173,12 +178,14 @@ dependencies {
|
||||||
|
|
||||||
implementation "com.github.CanHub:Android-Image-Cropper:4.1.0"
|
implementation "com.github.CanHub:Android-Image-Cropper:4.1.0"
|
||||||
|
|
||||||
implementation "de.c1710:filemojicompat:1.0.18"
|
implementation "de.c1710:filemojicompat-ui:$filemojicompat_version"
|
||||||
|
implementation "de.c1710:filemojicompat:$filemojicompat_version"
|
||||||
|
implementation "de.c1710:filemojicompat-defaults:$filemojicompat_version"
|
||||||
|
|
||||||
testImplementation "androidx.test.ext:junit:1.1.3"
|
testImplementation "androidx.test.ext:junit:1.1.3"
|
||||||
testImplementation "org.robolectric:robolectric:4.4"
|
testImplementation "org.robolectric:robolectric:4.4"
|
||||||
testImplementation "org.mockito:mockito-inline:3.6.28"
|
testImplementation "org.mockito:mockito-inline:4.4.0"
|
||||||
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0"
|
||||||
|
|
||||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
|
androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0"
|
||||||
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
androidTestImplementation "androidx.room:room-testing:$roomVersion"
|
||||||
|
|
815
app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json
Normal file
815
app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json
Normal file
|
@ -0,0 +1,815 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 32,
|
||||||
|
"identityHash": "c92343960c9d46d9cfd49f1873cce47d",
|
||||||
|
"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, `notificationsSignUps` 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": "notificationsSignUps",
|
||||||
|
"columnName": "notificationsSignUps",
|
||||||
|
"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, 'c92343960c9d46d9cfd49f1873cce47d')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
809
app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json
Normal file
809
app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json
Normal file
|
@ -0,0 +1,809 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 33,
|
||||||
|
"identityHash": "920a0e0c9a600bd236f6bf959b469c18",
|
||||||
|
"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, `notificationsSignUps` 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": "notificationsSignUps",
|
||||||
|
"columnName": "notificationsSignUps",
|
||||||
|
"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_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.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, '920a0e0c9a600bd236f6bf959b469c18')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
815
app/schemas/com.keylesspalace.tusky.db.AppDatabase/34.json
Normal file
815
app/schemas/com.keylesspalace.tusky.db.AppDatabase/34.json
Normal file
|
@ -0,0 +1,815 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 34,
|
||||||
|
"identityHash": "7f766d68ab5d72a7988cd81c183e9a9d",
|
||||||
|
"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, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)",
|
||||||
|
"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": "notificationsSignUps",
|
||||||
|
"columnName": "notificationsSignUps",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsUpdates",
|
||||||
|
"columnName": "notificationsUpdates",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationSound",
|
||||||
|
"columnName": "notificationSound",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationVibration",
|
||||||
|
"columnName": "notificationVibration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationLight",
|
||||||
|
"columnName": "notificationLight",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultPostPrivacy",
|
||||||
|
"columnName": "defaultPostPrivacy",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultMediaSensitivity",
|
||||||
|
"columnName": "defaultMediaSensitivity",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysShowSensitiveMedia",
|
||||||
|
"columnName": "alwaysShowSensitiveMedia",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysOpenSpoiler",
|
||||||
|
"columnName": "alwaysOpenSpoiler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaPreviewEnabled",
|
||||||
|
"columnName": "mediaPreviewEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastNotificationId",
|
||||||
|
"columnName": "lastNotificationId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activeNotifications",
|
||||||
|
"columnName": "activeNotifications",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tabPreferences",
|
||||||
|
"columnName": "tabPreferences",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFilter",
|
||||||
|
"columnName": "notificationsFilter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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_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.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, '7f766d68ab5d72a7988cd81c183e9a9d')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
821
app/schemas/com.keylesspalace.tusky.db.AppDatabase/35.json
Normal file
821
app/schemas/com.keylesspalace.tusky.db.AppDatabase/35.json
Normal file
|
@ -0,0 +1,821 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 35,
|
||||||
|
"identityHash": "9e6c0bb60538683a16c30fa3e1cc24f2",
|
||||||
|
"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, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)",
|
||||||
|
"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": "notificationsSignUps",
|
||||||
|
"columnName": "notificationsSignUps",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsUpdates",
|
||||||
|
"columnName": "notificationsUpdates",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationSound",
|
||||||
|
"columnName": "notificationSound",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationVibration",
|
||||||
|
"columnName": "notificationVibration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationLight",
|
||||||
|
"columnName": "notificationLight",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultPostPrivacy",
|
||||||
|
"columnName": "defaultPostPrivacy",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultMediaSensitivity",
|
||||||
|
"columnName": "defaultMediaSensitivity",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysShowSensitiveMedia",
|
||||||
|
"columnName": "alwaysShowSensitiveMedia",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysOpenSpoiler",
|
||||||
|
"columnName": "alwaysOpenSpoiler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaPreviewEnabled",
|
||||||
|
"columnName": "mediaPreviewEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastNotificationId",
|
||||||
|
"columnName": "lastNotificationId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activeNotifications",
|
||||||
|
"columnName": "activeNotifications",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tabPreferences",
|
||||||
|
"columnName": "tabPreferences",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFilter",
|
||||||
|
"columnName": "notificationsFilter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorServerId",
|
||||||
|
"columnName": "authorServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToAccountId",
|
||||||
|
"columnName": "inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogsCount",
|
||||||
|
"columnName": "reblogsCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favouritesCount",
|
||||||
|
"columnName": "favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tags",
|
||||||
|
"columnName": "tags",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "application",
|
||||||
|
"columnName": "application",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogServerId",
|
||||||
|
"columnName": "reblogServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogAccountId",
|
||||||
|
"columnName": "reblogAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "muted",
|
||||||
|
"columnName": "muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "expanded",
|
||||||
|
"columnName": "expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentCollapsed",
|
||||||
|
"columnName": "contentCollapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentShowing",
|
||||||
|
"columnName": "contentShowing",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pinned",
|
||||||
|
"columnName": "pinned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "card",
|
||||||
|
"columnName": "card",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "TimelineAccountEntity",
|
||||||
|
"onDelete": "NO ACTION",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineAccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "localUsername",
|
||||||
|
"columnName": "localUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatar",
|
||||||
|
"columnName": "avatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bot",
|
||||||
|
"columnName": "bot",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ConversationEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `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_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.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, '9e6c0bb60538683a16c30fa3e1cc24f2')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,8 +35,7 @@ import androidx.appcompat.app.AlertDialog
|
||||||
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.emoji.text.EmojiCompat
|
import androidx.core.view.GravityCompat
|
||||||
import androidx.emoji.text.EmojiCompat.InitCallback
|
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
@ -114,6 +113,7 @@ import com.mikepenz.materialdrawer.util.updateBadge
|
||||||
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
import com.mikepenz.materialdrawer.widget.AccountHeaderView
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.android.DispatchingAndroidInjector
|
||||||
import dagger.android.HasAndroidInjector
|
import dagger.android.HasAndroidInjector
|
||||||
|
import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -150,13 +150,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
|
|
||||||
private var accountLocked: Boolean = false
|
private var accountLocked: Boolean = false
|
||||||
|
|
||||||
private val emojiInitCallback = object : InitCallback() {
|
// We need to know if the emoji pack has been changed
|
||||||
override fun onInitialized() {
|
private var selectedEmojiPack: String? = null
|
||||||
if (!isDestroyed) {
|
|
||||||
updateProfiles()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -271,11 +266,31 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
// Flush old media that was cached for sharing
|
// Flush old media that was cached for sharing
|
||||||
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
|
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectedEmojiPack = preferences.getString(EMOJI_PREFERENCE, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
NotificationHelper.clearNotificationsForActiveAccount(this, accountManager)
|
NotificationHelper.clearNotificationsForActiveAccount(this, accountManager)
|
||||||
|
val currentEmojiPack = preferences.getString(EMOJI_PREFERENCE, "")
|
||||||
|
if (currentEmojiPack != selectedEmojiPack) {
|
||||||
|
Log.d(
|
||||||
|
TAG,
|
||||||
|
"onResume: EmojiPack has been changed from %s to %s"
|
||||||
|
.format(selectedEmojiPack, currentEmojiPack)
|
||||||
|
)
|
||||||
|
selectedEmojiPack = currentEmojiPack
|
||||||
|
recreate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
// For some reason the navigation drawer is opened when the activity is recreated
|
||||||
|
if (binding.mainDrawerLayout.isOpen) {
|
||||||
|
binding.mainDrawerLayout.closeDrawer(GravityCompat.START, false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
|
@ -333,11 +348,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
EmojiCompat.get().unregisterInitCallback(emojiInitCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun forwardShare(intent: Intent) {
|
private fun forwardShare(intent: Intent) {
|
||||||
val composeIntent = Intent(this, ComposeActivity::class.java)
|
val composeIntent = Intent(this, ComposeActivity::class.java)
|
||||||
composeIntent.action = intent.action
|
composeIntent.action = intent.action
|
||||||
|
@ -530,7 +540,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
EmojiCompat.get().registerInitCallback(emojiInitCallback)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
|
@ -612,6 +621,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
binding.mainToolbar.setOnClickListener {
|
binding.mainToolbar.setOnClickListener {
|
||||||
(adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
|
(adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateProfiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean {
|
private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean {
|
||||||
|
@ -682,18 +693,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchUserInfo() {
|
private fun fetchUserInfo() = lifecycleScope.launch {
|
||||||
mastodonApi.accountVerifyCredentials()
|
mastodonApi.accountVerifyCredentials().fold(
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
{ userInfo ->
|
||||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
onFetchUserInfoSuccess(userInfo)
|
||||||
.subscribe(
|
},
|
||||||
{ userInfo ->
|
{ throwable ->
|
||||||
onFetchUserInfoSuccess(userInfo)
|
Log.e(TAG, "Failed to fetch user info. " + throwable.message)
|
||||||
},
|
}
|
||||||
{ throwable ->
|
)
|
||||||
Log.e(TAG, "Failed to fetch user info. " + throwable.message)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFetchUserInfoSuccess(me: Account) {
|
private fun onFetchUserInfoSuccess(me: Account) {
|
||||||
|
@ -782,18 +790,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fetchAnnouncements() {
|
private fun fetchAnnouncements() {
|
||||||
mastodonApi.listAnnouncements(false)
|
lifecycleScope.launch {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
mastodonApi.listAnnouncements(false)
|
||||||
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
.fold(
|
||||||
.subscribe(
|
{ announcements ->
|
||||||
{ announcements ->
|
unreadAnnouncementsCount = announcements.count { !it.read }
|
||||||
unreadAnnouncementsCount = announcements.count { !it.read }
|
updateAnnouncementsBadge()
|
||||||
updateAnnouncementsBadge()
|
},
|
||||||
},
|
{ throwable ->
|
||||||
{
|
Log.w(TAG, "Failed to fetch announcements.", throwable)
|
||||||
Log.w(TAG, "Failed to fetch announcements.", it)
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateAnnouncementsBadge() {
|
private fun updateAnnouncementsBadge() {
|
||||||
|
@ -803,11 +811,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
private fun updateProfiles() {
|
private fun updateProfiles() {
|
||||||
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false)
|
||||||
val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc ->
|
val profiles: MutableList<IProfile> = accountManager.getAllAccountsOrderedByActive().map { acc ->
|
||||||
val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis))
|
|
||||||
|
|
||||||
ProfileDrawerItem().apply {
|
ProfileDrawerItem().apply {
|
||||||
isSelected = acc.isActive
|
isSelected = acc.isActive
|
||||||
nameText = emojifiedName
|
nameText = acc.displayName.emojify(acc.emojis, header, animateEmojis)
|
||||||
iconUrl = acc.profilePictureUrl
|
iconUrl = acc.profilePictureUrl
|
||||||
isNameShown = true
|
isNameShown = true
|
||||||
identifier = acc.id
|
identifier = acc.id
|
||||||
|
|
|
@ -19,18 +19,18 @@ import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.emoji.text.EmojiCompat
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import autodispose2.AutoDisposePlugins
|
import autodispose2.AutoDisposePlugins
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
|
import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory
|
||||||
import com.keylesspalace.tusky.di.AppInjector
|
import com.keylesspalace.tusky.di.AppInjector
|
||||||
import com.keylesspalace.tusky.settings.PrefKeys
|
|
||||||
import com.keylesspalace.tusky.util.EmojiCompatFont
|
|
||||||
import com.keylesspalace.tusky.util.LocaleManager
|
import com.keylesspalace.tusky.util.LocaleManager
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import dagger.android.DispatchingAndroidInjector
|
import dagger.android.DispatchingAndroidInjector
|
||||||
import dagger.android.HasAndroidInjector
|
import dagger.android.HasAndroidInjector
|
||||||
|
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
|
||||||
|
import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper
|
||||||
|
import de.c1710.filemojicompat_ui.helpers.EmojiPreference
|
||||||
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
import io.reactivex.rxjava3.plugins.RxJavaPlugins
|
||||||
import org.conscrypt.Conscrypt
|
import org.conscrypt.Conscrypt
|
||||||
import java.security.Security
|
import java.security.Security
|
||||||
|
@ -65,12 +65,10 @@ class TuskyApplication : Application(), HasAndroidInjector {
|
||||||
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
// init the custom emoji fonts
|
// In this case, we want to have the emoji preferences merged with the other ones
|
||||||
val emojiSelection = preferences.getInt(PrefKeys.EMOJI, 0)
|
// Copied from PreferenceManager.getDefaultSharedPreferenceName
|
||||||
val emojiConfig = EmojiCompatFont.byId(emojiSelection)
|
EmojiPreference.sharedPreferenceName = packageName + "_preferences"
|
||||||
.getConfig(this)
|
EmojiPackHelper.init(this, DefaultEmojiPackList.get(this), allowPackImports = false)
|
||||||
.setReplaceAll(true)
|
|
||||||
EmojiCompat.init(emojiConfig)
|
|
||||||
|
|
||||||
// init night mode
|
// init night mode
|
||||||
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
|
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
|
||||||
|
|
|
@ -283,7 +283,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
|
||||||
}
|
}
|
||||||
return@fromCallable false
|
return@fromCallable false
|
||||||
}
|
}
|
||||||
|
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnDispose {
|
.doOnDispose {
|
||||||
|
|
|
@ -45,8 +45,7 @@ public class AccountViewHolder extends RecyclerView.ViewHolder {
|
||||||
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
|
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
|
||||||
if (showBotOverlay && account.getBot()) {
|
if (showBotOverlay && account.getBot()) {
|
||||||
avatarInset.setVisibility(View.VISIBLE);
|
avatarInset.setVisibility(View.VISIBLE);
|
||||||
avatarInset.setImageResource(R.drawable.ic_bot_24dp);
|
avatarInset.setImageResource(R.drawable.bot_badge);
|
||||||
avatarInset.setBackgroundColor(0x50ffffff);
|
|
||||||
} else {
|
} else {
|
||||||
avatarInset.setVisibility(View.GONE);
|
avatarInset.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,8 @@ import android.widget.Button;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorRes;
|
||||||
|
import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
@ -47,6 +49,7 @@ 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;
|
||||||
|
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter;
|
||||||
import com.keylesspalace.tusky.util.CardViewMode;
|
import com.keylesspalace.tusky.util.CardViewMode;
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||||
|
@ -58,10 +61,8 @@ import com.keylesspalace.tusky.util.TimestampUtils;
|
||||||
import com.keylesspalace.tusky.viewdata.NotificationViewData;
|
import com.keylesspalace.tusky.viewdata.NotificationViewData;
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import at.connyduck.sparkbutton.helpers.Utils;
|
import at.connyduck.sparkbutton.helpers.Utils;
|
||||||
|
|
||||||
|
@ -90,6 +91,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
private NotificationActionListener notificationActionListener;
|
private NotificationActionListener notificationActionListener;
|
||||||
private AccountActionListener accountActionListener;
|
private AccountActionListener accountActionListener;
|
||||||
private AdapterDataSource<NotificationViewData> dataSource;
|
private AdapterDataSource<NotificationViewData> dataSource;
|
||||||
|
private final AbsoluteTimeFormatter absoluteTimeFormatter = new AbsoluteTimeFormatter();
|
||||||
|
|
||||||
public NotificationsAdapter(String accountId,
|
public NotificationsAdapter(String accountId,
|
||||||
AdapterDataSource<NotificationViewData> dataSource,
|
AdapterDataSource<NotificationViewData> dataSource,
|
||||||
|
@ -119,7 +121,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
case VIEW_TYPE_STATUS_NOTIFICATION: {
|
case VIEW_TYPE_STATUS_NOTIFICATION: {
|
||||||
View view = inflater
|
View view = inflater
|
||||||
.inflate(R.layout.item_status_notification, parent, false);
|
.inflate(R.layout.item_status_notification, parent, false);
|
||||||
return new StatusNotificationViewHolder(view, statusDisplayOptions);
|
return new StatusNotificationViewHolder(view, statusDisplayOptions, absoluteTimeFormatter);
|
||||||
}
|
}
|
||||||
case VIEW_TYPE_FOLLOW: {
|
case VIEW_TYPE_FOLLOW: {
|
||||||
View view = inflater
|
View view = inflater
|
||||||
|
@ -178,8 +180,16 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
case VIEW_TYPE_STATUS: {
|
case VIEW_TYPE_STATUS: {
|
||||||
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
StatusViewHolder holder = (StatusViewHolder) viewHolder;
|
||||||
StatusViewData.Concrete status = concreteNotificaton.getStatusViewData();
|
StatusViewData.Concrete status = concreteNotificaton.getStatusViewData();
|
||||||
holder.setupWithStatus(status,
|
if (status == null) {
|
||||||
statusListener, statusDisplayOptions, payloadForHolder);
|
/* in some very rare cases servers sends null status even though they should not,
|
||||||
|
* we have to handle it somehow */
|
||||||
|
holder.showStatusContent(false);
|
||||||
|
} else {
|
||||||
|
if (payloads == null) {
|
||||||
|
holder.showStatusContent(true);
|
||||||
|
}
|
||||||
|
holder.setupWithStatus(status, statusListener, statusDisplayOptions, payloadForHolder);
|
||||||
|
}
|
||||||
if (concreteNotificaton.getType() == Notification.Type.POLL) {
|
if (concreteNotificaton.getType() == Notification.Type.POLL) {
|
||||||
holder.setPollInfo(accountId.equals(concreteNotificaton.getAccount().getId()));
|
holder.setPollInfo(accountId.equals(concreteNotificaton.getAccount().getId()));
|
||||||
} else {
|
} else {
|
||||||
|
@ -192,6 +202,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
StatusViewData.Concrete statusViewData = concreteNotificaton.getStatusViewData();
|
StatusViewData.Concrete statusViewData = concreteNotificaton.getStatusViewData();
|
||||||
if (payloadForHolder == null) {
|
if (payloadForHolder == null) {
|
||||||
if (statusViewData == null) {
|
if (statusViewData == null) {
|
||||||
|
/* in some very rare cases servers sends null status even though they should not,
|
||||||
|
* we have to handle it somehow */
|
||||||
holder.showNotificationContent(false);
|
holder.showNotificationContent(false);
|
||||||
} else {
|
} else {
|
||||||
holder.showNotificationContent(true);
|
holder.showNotificationContent(true);
|
||||||
|
@ -201,7 +213,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
holder.setUsername(status.getAccount().getUsername());
|
holder.setUsername(status.getAccount().getUsername());
|
||||||
holder.setCreatedAt(status.getCreatedAt());
|
holder.setCreatedAt(status.getCreatedAt());
|
||||||
|
|
||||||
if (concreteNotificaton.getType() == Notification.Type.STATUS) {
|
if (concreteNotificaton.getType() == Notification.Type.STATUS ||
|
||||||
|
concreteNotificaton.getType() == Notification.Type.UPDATE) {
|
||||||
holder.setAvatar(status.getAccount().getAvatar(), status.getAccount().getBot());
|
holder.setAvatar(status.getAccount().getAvatar(), status.getAccount().getBot());
|
||||||
} else {
|
} else {
|
||||||
holder.setAvatars(status.getAccount().getAvatar(),
|
holder.setAvatars(status.getAccount().getAvatar(),
|
||||||
|
@ -226,7 +239,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
case VIEW_TYPE_FOLLOW: {
|
case VIEW_TYPE_FOLLOW: {
|
||||||
if (payloadForHolder == null) {
|
if (payloadForHolder == null) {
|
||||||
FollowViewHolder holder = (FollowViewHolder) viewHolder;
|
FollowViewHolder holder = (FollowViewHolder) viewHolder;
|
||||||
holder.setMessage(concreteNotificaton.getAccount());
|
holder.setMessage(concreteNotificaton.getAccount(), concreteNotificaton.getType() == Notification.Type.SIGN_UP);
|
||||||
holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId());
|
holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId());
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -280,10 +293,12 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
}
|
}
|
||||||
case STATUS:
|
case STATUS:
|
||||||
case FAVOURITE:
|
case FAVOURITE:
|
||||||
case REBLOG: {
|
case REBLOG:
|
||||||
|
case UPDATE: {
|
||||||
return VIEW_TYPE_STATUS_NOTIFICATION;
|
return VIEW_TYPE_STATUS_NOTIFICATION;
|
||||||
}
|
}
|
||||||
case FOLLOW: {
|
case FOLLOW:
|
||||||
|
case SIGN_UP: {
|
||||||
return VIEW_TYPE_FOLLOW;
|
return VIEW_TYPE_FOLLOW;
|
||||||
}
|
}
|
||||||
case FOLLOW_REQUEST: {
|
case FOLLOW_REQUEST: {
|
||||||
|
@ -335,10 +350,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
this.statusDisplayOptions = statusDisplayOptions;
|
this.statusDisplayOptions = statusDisplayOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMessage(TimelineAccount account) {
|
void setMessage(TimelineAccount account, Boolean isSignUp) {
|
||||||
Context context = message.getContext();
|
Context context = message.getContext();
|
||||||
|
|
||||||
String format = context.getString(R.string.notification_follow_format);
|
String format = context.getString(isSignUp ? R.string.notification_sign_up_format : R.string.notification_follow_format);
|
||||||
String wrappedDisplayName = StringUtils.unicodeWrap(account.getName());
|
String wrappedDisplayName = StringUtils.unicodeWrap(account.getName());
|
||||||
String wholeMessage = String.format(format, wrappedDisplayName);
|
String wholeMessage = String.format(format, wrappedDisplayName);
|
||||||
CharSequence emojifiedMessage = CustomEmojiHelper.emojify(
|
CharSequence emojifiedMessage = CustomEmojiHelper.emojify(
|
||||||
|
@ -382,19 +397,22 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
private final Button contentWarningButton;
|
private final Button contentWarningButton;
|
||||||
private final Button contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder
|
private final Button contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder
|
||||||
private StatusDisplayOptions statusDisplayOptions;
|
private StatusDisplayOptions statusDisplayOptions;
|
||||||
|
private final AbsoluteTimeFormatter absoluteTimeFormatter;
|
||||||
|
|
||||||
private String accountId;
|
private String accountId;
|
||||||
private String notificationId;
|
private String notificationId;
|
||||||
private NotificationActionListener notificationActionListener;
|
private NotificationActionListener notificationActionListener;
|
||||||
private StatusViewData.Concrete statusViewData;
|
private StatusViewData.Concrete statusViewData;
|
||||||
private SimpleDateFormat shortSdf;
|
|
||||||
private SimpleDateFormat longSdf;
|
|
||||||
|
|
||||||
private int avatarRadius48dp;
|
private int avatarRadius48dp;
|
||||||
private int avatarRadius36dp;
|
private int avatarRadius36dp;
|
||||||
private int avatarRadius24dp;
|
private int avatarRadius24dp;
|
||||||
|
|
||||||
StatusNotificationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) {
|
StatusNotificationViewHolder(
|
||||||
|
View itemView,
|
||||||
|
StatusDisplayOptions statusDisplayOptions,
|
||||||
|
AbsoluteTimeFormatter absoluteTimeFormatter
|
||||||
|
) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
message = itemView.findViewById(R.id.notification_top_text);
|
message = itemView.findViewById(R.id.notification_top_text);
|
||||||
statusNameBar = itemView.findViewById(R.id.status_name_bar);
|
statusNameBar = itemView.findViewById(R.id.status_name_bar);
|
||||||
|
@ -408,6 +426,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
contentWarningButton = itemView.findViewById(R.id.notification_content_warning_button);
|
contentWarningButton = itemView.findViewById(R.id.notification_content_warning_button);
|
||||||
contentCollapseButton = itemView.findViewById(R.id.button_toggle_notification_content);
|
contentCollapseButton = itemView.findViewById(R.id.button_toggle_notification_content);
|
||||||
this.statusDisplayOptions = statusDisplayOptions;
|
this.statusDisplayOptions = statusDisplayOptions;
|
||||||
|
this.absoluteTimeFormatter = absoluteTimeFormatter;
|
||||||
|
|
||||||
int darkerFilter = Color.rgb(123, 123, 123);
|
int darkerFilter = Color.rgb(123, 123, 123);
|
||||||
statusAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY);
|
statusAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY);
|
||||||
|
@ -416,8 +435,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
itemView.setOnClickListener(this);
|
itemView.setOnClickListener(this);
|
||||||
message.setOnClickListener(this);
|
message.setOnClickListener(this);
|
||||||
statusContent.setOnClickListener(this);
|
statusContent.setOnClickListener(this);
|
||||||
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
|
|
||||||
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
|
|
||||||
|
|
||||||
this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp);
|
this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp);
|
||||||
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
|
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
|
||||||
|
@ -447,17 +464,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
|
|
||||||
protected void setCreatedAt(@Nullable Date createdAt) {
|
protected void setCreatedAt(@Nullable Date createdAt) {
|
||||||
if (statusDisplayOptions.useAbsoluteTime()) {
|
if (statusDisplayOptions.useAbsoluteTime()) {
|
||||||
String time;
|
timestampInfo.setText(absoluteTimeFormatter.format(createdAt, true));
|
||||||
if (createdAt != null) {
|
|
||||||
if (System.currentTimeMillis() - createdAt.getTime() > 86400000L) {
|
|
||||||
time = longSdf.format(createdAt);
|
|
||||||
} else {
|
|
||||||
time = shortSdf.format(createdAt);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
time = "??:??:??";
|
|
||||||
}
|
|
||||||
timestampInfo.setText(time);
|
|
||||||
} else {
|
} else {
|
||||||
// This is the visible timestampInfo.
|
// This is the visible timestampInfo.
|
||||||
String readout;
|
String readout;
|
||||||
|
@ -481,6 +488,14 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Drawable getIconWithColor(Context context, @DrawableRes int drawable, @ColorRes int color) {
|
||||||
|
Drawable icon = ContextCompat.getDrawable(context, drawable);
|
||||||
|
if (icon != null) {
|
||||||
|
icon.setColorFilter(ContextCompat.getColor(context, color), PorterDuff.Mode.SRC_ATOP);
|
||||||
|
}
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener) {
|
void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener) {
|
||||||
this.statusViewData = notificationViewData.getStatusViewData();
|
this.statusViewData = notificationViewData.getStatusViewData();
|
||||||
|
|
||||||
|
@ -493,41 +508,36 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
default:
|
default:
|
||||||
case FAVOURITE: {
|
case FAVOURITE: {
|
||||||
icon = ContextCompat.getDrawable(context, R.drawable.ic_star_24dp);
|
icon = getIconWithColor(context, R.drawable.ic_star_24dp, R.color.tusky_orange);
|
||||||
if (icon != null) {
|
|
||||||
icon.setColorFilter(ContextCompat.getColor(context,
|
|
||||||
R.color.tusky_orange), PorterDuff.Mode.SRC_ATOP);
|
|
||||||
}
|
|
||||||
|
|
||||||
format = context.getString(R.string.notification_favourite_format);
|
format = context.getString(R.string.notification_favourite_format);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case REBLOG: {
|
case REBLOG: {
|
||||||
icon = ContextCompat.getDrawable(context, R.drawable.ic_repeat_24dp);
|
icon = getIconWithColor(context, R.drawable.ic_repeat_24dp, R.color.chinwag_green);
|
||||||
if (icon != null) {
|
|
||||||
icon.setColorFilter(ContextCompat.getColor(context,
|
|
||||||
R.color.chinwag_green), PorterDuff.Mode.SRC_ATOP);
|
|
||||||
}
|
|
||||||
|
|
||||||
format = context.getString(R.string.notification_reblog_format);
|
format = context.getString(R.string.notification_reblog_format);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case STATUS: {
|
case STATUS: {
|
||||||
icon = ContextCompat.getDrawable(context, R.drawable.ic_home_24dp);
|
icon = getIconWithColor(context, R.drawable.ic_home_24dp, R.color.chinwag_green);
|
||||||
if (icon != null) {
|
|
||||||
icon.setColorFilter(ContextCompat.getColor(context,
|
|
||||||
R.color.chinwag_green), PorterDuff.Mode.SRC_ATOP);
|
|
||||||
}
|
|
||||||
|
|
||||||
format = context.getString(R.string.notification_subscription_format);
|
format = context.getString(R.string.notification_subscription_format);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case UPDATE: {
|
||||||
|
icon = getIconWithColor(context, R.drawable.ic_edit_24dp, R.color.chinwag_green);
|
||||||
|
format = context.getString(R.string.notification_update_format);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
|
message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null);
|
||||||
String wholeMessage = String.format(format, displayName);
|
String wholeMessage = String.format(format, displayName);
|
||||||
final SpannableStringBuilder str = new SpannableStringBuilder(wholeMessage);
|
final SpannableStringBuilder str = new SpannableStringBuilder(wholeMessage);
|
||||||
str.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(),
|
int displayNameIndex = format.indexOf("%s");
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
str.setSpan(
|
||||||
|
new StyleSpan(Typeface.BOLD),
|
||||||
|
displayNameIndex,
|
||||||
|
displayNameIndex + displayName.length(),
|
||||||
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||||
|
);
|
||||||
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
CharSequence emojifiedText = CustomEmojiHelper.emojify(
|
||||||
str, notificationViewData.getAccount().getEmojis(), message, statusDisplayOptions.animateEmojis()
|
str, notificationViewData.getAccount().getEmojis(), message, statusDisplayOptions.animateEmojis()
|
||||||
);
|
);
|
||||||
|
@ -570,9 +580,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
|
|
||||||
if (statusDisplayOptions.showBotOverlay() && isBot) {
|
if (statusDisplayOptions.showBotOverlay() && isBot) {
|
||||||
notificationAvatar.setVisibility(View.VISIBLE);
|
notificationAvatar.setVisibility(View.VISIBLE);
|
||||||
notificationAvatar.setBackgroundColor(0x50ffffff);
|
|
||||||
Glide.with(notificationAvatar)
|
Glide.with(notificationAvatar)
|
||||||
.load(R.drawable.ic_bot_24dp)
|
.load(ContextCompat.getDrawable(notificationAvatar.getContext(), R.drawable.bot_badge))
|
||||||
.into(notificationAvatar);
|
.into(notificationAvatar);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -19,7 +19,6 @@ import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.emoji.text.EmojiCompat
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ItemPollBinding
|
import com.keylesspalace.tusky.databinding.ItemPollBinding
|
||||||
|
@ -87,9 +86,8 @@ class PollAdapter : RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
||||||
when (mode) {
|
when (mode) {
|
||||||
RESULT -> {
|
RESULT -> {
|
||||||
val percent = calculatePercent(option.votesCount, votersCount, voteCount)
|
val percent = calculatePercent(option.votesCount, votersCount, voteCount)
|
||||||
val emojifiedPollOptionText = buildDescription(option.title, percent, option.voted, resultTextView.context)
|
resultTextView.text = buildDescription(option.title, percent, option.voted, resultTextView.context)
|
||||||
.emojify(emojis, resultTextView, animateEmojis)
|
.emojify(emojis, resultTextView, animateEmojis)
|
||||||
resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText)
|
|
||||||
|
|
||||||
val level = percent * 100
|
val level = percent * 100
|
||||||
val optionColor = if (option.voted) {
|
val optionColor = if (option.voted) {
|
||||||
|
@ -103,8 +101,7 @@ class PollAdapter : RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
||||||
resultTextView.setOnClickListener(resultClickListener)
|
resultTextView.setOnClickListener(resultClickListener)
|
||||||
}
|
}
|
||||||
SINGLE -> {
|
SINGLE -> {
|
||||||
val emojifiedPollOptionText = option.title.emojify(emojis, radioButton, animateEmojis)
|
radioButton.text = option.title.emojify(emojis, radioButton, animateEmojis)
|
||||||
radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText)
|
|
||||||
radioButton.isChecked = option.selected
|
radioButton.isChecked = option.selected
|
||||||
radioButton.setOnClickListener {
|
radioButton.setOnClickListener {
|
||||||
pollOptions.forEachIndexed { index, pollOption ->
|
pollOptions.forEachIndexed { index, pollOption ->
|
||||||
|
@ -114,8 +111,7 @@ class PollAdapter : RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MULTIPLE -> {
|
MULTIPLE -> {
|
||||||
val emojifiedPollOptionText = option.title.emojify(emojis, checkBox, animateEmojis)
|
checkBox.text = option.title.emojify(emojis, checkBox, animateEmojis)
|
||||||
checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText)
|
|
||||||
checkBox.isChecked = option.selected
|
checkBox.isChecked = option.selected
|
||||||
checkBox.setOnCheckedChangeListener { _, isChecked ->
|
checkBox.setOnCheckedChangeListener { _, isChecked ->
|
||||||
pollOptions[holder.bindingAdapterPosition].selected = isChecked
|
pollOptions[holder.bindingAdapterPosition].selected = isChecked
|
||||||
|
|
|
@ -20,6 +20,8 @@ import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.text.HtmlCompat;
|
import androidx.core.text.HtmlCompat;
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
@ -27,6 +29,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.RequestBuilder;
|
import com.bumptech.glide.RequestBuilder;
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||||
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
|
import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners;
|
||||||
import com.google.android.material.button.MaterialButton;
|
import com.google.android.material.button.MaterialButton;
|
||||||
|
@ -40,6 +43,7 @@ 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.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
|
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter;
|
||||||
import com.keylesspalace.tusky.util.CardViewMode;
|
import com.keylesspalace.tusky.util.CardViewMode;
|
||||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||||
|
@ -54,10 +58,8 @@ import com.keylesspalace.tusky.viewdata.PollViewDataKt;
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
import java.text.NumberFormat;
|
import java.text.NumberFormat;
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import at.connyduck.sparkbutton.SparkButton;
|
import at.connyduck.sparkbutton.SparkButton;
|
||||||
import at.connyduck.sparkbutton.helpers.Utils;
|
import at.connyduck.sparkbutton.helpers.Utils;
|
||||||
|
@ -77,6 +79,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
private SparkButton favouriteButton;
|
private SparkButton favouriteButton;
|
||||||
private SparkButton bookmarkButton;
|
private SparkButton bookmarkButton;
|
||||||
private ImageButton moreButton;
|
private ImageButton moreButton;
|
||||||
|
private ConstraintLayout mediaContainer;
|
||||||
protected MediaPreviewImageView[] mediaPreviews;
|
protected MediaPreviewImageView[] mediaPreviews;
|
||||||
private ImageView[] mediaOverlays;
|
private ImageView[] mediaOverlays;
|
||||||
private TextView sensitiveMediaWarning;
|
private TextView sensitiveMediaWarning;
|
||||||
|
@ -103,10 +106,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
private TextView cardUrl;
|
private TextView cardUrl;
|
||||||
private PollAdapter pollAdapter;
|
private PollAdapter pollAdapter;
|
||||||
|
|
||||||
private SimpleDateFormat shortSdf;
|
|
||||||
private SimpleDateFormat longSdf;
|
|
||||||
|
|
||||||
private final NumberFormat numberFormat = NumberFormat.getNumberInstance();
|
private final NumberFormat numberFormat = NumberFormat.getNumberInstance();
|
||||||
|
private final AbsoluteTimeFormatter absoluteTimeFormatter = new AbsoluteTimeFormatter();
|
||||||
|
|
||||||
protected int avatarRadius48dp;
|
protected int avatarRadius48dp;
|
||||||
private int avatarRadius36dp;
|
private int avatarRadius36dp;
|
||||||
|
@ -127,7 +128,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
bookmarkButton = itemView.findViewById(R.id.status_bookmark);
|
bookmarkButton = itemView.findViewById(R.id.status_bookmark);
|
||||||
moreButton = itemView.findViewById(R.id.status_more);
|
moreButton = itemView.findViewById(R.id.status_more);
|
||||||
|
|
||||||
itemView.findViewById(R.id.status_media_preview_container).setClipToOutline(true);
|
mediaContainer = itemView.findViewById(R.id.status_media_preview_container);
|
||||||
|
mediaContainer.setClipToOutline(true);
|
||||||
|
|
||||||
mediaPreviews = new MediaPreviewImageView[]{
|
mediaPreviews = new MediaPreviewImageView[]{
|
||||||
itemView.findViewById(R.id.status_media_preview_0),
|
itemView.findViewById(R.id.status_media_preview_0),
|
||||||
|
@ -170,9 +172,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext()));
|
pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext()));
|
||||||
((DefaultItemAnimator) pollOptions.getItemAnimator()).setSupportsChangeAnimations(false);
|
((DefaultItemAnimator) pollOptions.getItemAnimator()).setSupportsChangeAnimations(false);
|
||||||
|
|
||||||
this.shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
|
|
||||||
this.longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
|
|
||||||
|
|
||||||
this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp);
|
this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp);
|
||||||
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
|
this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp);
|
||||||
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
|
this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp);
|
||||||
|
@ -290,11 +289,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
if (statusDisplayOptions.showBotOverlay() && isBot) {
|
if (statusDisplayOptions.showBotOverlay() && isBot) {
|
||||||
avatarInset.setVisibility(View.VISIBLE);
|
avatarInset.setVisibility(View.VISIBLE);
|
||||||
avatarInset.setBackgroundColor(0x50ffffff);
|
|
||||||
Glide.with(avatarInset)
|
Glide.with(avatarInset)
|
||||||
.load(R.drawable.ic_bot_24dp)
|
// passing the drawable id directly into .load() ignores night mode https://github.com/bumptech/glide/issues/4692
|
||||||
|
.load(ContextCompat.getDrawable(avatarInset.getContext(), R.drawable.bot_badge))
|
||||||
.into(avatarInset);
|
.into(avatarInset);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
avatarInset.setVisibility(View.GONE);
|
avatarInset.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
@ -320,7 +318,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
protected void setCreatedAt(Date createdAt, StatusDisplayOptions statusDisplayOptions) {
|
protected void setCreatedAt(Date createdAt, StatusDisplayOptions statusDisplayOptions) {
|
||||||
if (statusDisplayOptions.useAbsoluteTime()) {
|
if (statusDisplayOptions.useAbsoluteTime()) {
|
||||||
timestampInfo.setText(getAbsoluteTime(createdAt));
|
timestampInfo.setText(absoluteTimeFormatter.format(createdAt, true));
|
||||||
} else {
|
} else {
|
||||||
if (createdAt == null) {
|
if (createdAt == null) {
|
||||||
timestampInfo.setText("?m");
|
timestampInfo.setText("?m");
|
||||||
|
@ -333,21 +331,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getAbsoluteTime(Date createdAt) {
|
|
||||||
if (createdAt == null) {
|
|
||||||
return "??:??:??";
|
|
||||||
}
|
|
||||||
if (DateUtils.isToday(createdAt.getTime())) {
|
|
||||||
return shortSdf.format(createdAt);
|
|
||||||
} else {
|
|
||||||
return longSdf.format(createdAt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private CharSequence getCreatedAtDescription(Date createdAt,
|
private CharSequence getCreatedAtDescription(Date createdAt,
|
||||||
StatusDisplayOptions statusDisplayOptions) {
|
StatusDisplayOptions statusDisplayOptions) {
|
||||||
if (statusDisplayOptions.useAbsoluteTime()) {
|
if (statusDisplayOptions.useAbsoluteTime()) {
|
||||||
return getAbsoluteTime(createdAt);
|
return absoluteTimeFormatter.format(createdAt, true);
|
||||||
} else {
|
} else {
|
||||||
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
|
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
|
||||||
* as 17 meters instead of minutes. */
|
* as 17 meters instead of minutes. */
|
||||||
|
@ -736,9 +723,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
this.setupWithStatus(status, listener, statusDisplayOptions, null);
|
this.setupWithStatus(status, listener, statusDisplayOptions, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setupWithStatus(StatusViewData.Concrete status,
|
public void setupWithStatus(@NonNull StatusViewData.Concrete status,
|
||||||
final StatusActionListener listener,
|
@NonNull final StatusActionListener listener,
|
||||||
StatusDisplayOptions statusDisplayOptions,
|
@NonNull StatusDisplayOptions statusDisplayOptions,
|
||||||
@Nullable Object payloads) {
|
@Nullable Object payloads) {
|
||||||
if (payloads == null) {
|
if (payloads == null) {
|
||||||
Status actionable = status.getActionable();
|
Status actionable = status.getActionable();
|
||||||
|
@ -1028,7 +1015,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
return votesText;
|
return votesText;
|
||||||
} else {
|
} else {
|
||||||
if (statusDisplayOptions.useAbsoluteTime()) {
|
if (statusDisplayOptions.useAbsoluteTime()) {
|
||||||
pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.getExpiresAt()));
|
pollDurationInfo = context.getString(R.string.poll_info_time_absolute, absoluteTimeFormatter.format(poll.getExpiresAt(), false));
|
||||||
} else {
|
} else {
|
||||||
pollDurationInfo = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp);
|
pollDurationInfo = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp);
|
||||||
}
|
}
|
||||||
|
@ -1043,9 +1030,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
StatusDisplayOptions statusDisplayOptions,
|
StatusDisplayOptions statusDisplayOptions,
|
||||||
final StatusActionListener listener
|
final StatusActionListener listener
|
||||||
) {
|
) {
|
||||||
final Card card = status.getActionable().getCard();
|
final Status actionable = status.getActionable();
|
||||||
|
final Card card = actionable.getCard();
|
||||||
if (cardViewMode != CardViewMode.NONE &&
|
if (cardViewMode != CardViewMode.NONE &&
|
||||||
status.getActionable().getAttachments().size() == 0 &&
|
actionable.getAttachments().size() == 0 &&
|
||||||
|
actionable.getPoll() == null &&
|
||||||
card != null &&
|
card != null &&
|
||||||
!TextUtils.isEmpty(card.getUrl()) &&
|
!TextUtils.isEmpty(card.getUrl()) &&
|
||||||
(!status.isCollapsible() || !status.isCollapsed())) {
|
(!status.isCollapsible() || !status.isCollapsed())) {
|
||||||
|
@ -1067,7 +1056,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
// Statuses from other activitypub sources can be marked sensitive even if there's no media,
|
// Statuses from other activitypub sources can be marked sensitive even if there's no media,
|
||||||
// so let's blur the preview in that case
|
// so let's blur the preview in that case
|
||||||
// If media previews are disabled, show placeholder for cards as well
|
// If media previews are disabled, show placeholder for cards as well
|
||||||
if (statusDisplayOptions.mediaPreviewEnabled() && !status.getActionable().getSensitive() && !TextUtils.isEmpty(card.getImage())) {
|
if (statusDisplayOptions.mediaPreviewEnabled() && !actionable.getSensitive() && !TextUtils.isEmpty(card.getImage())) {
|
||||||
|
|
||||||
int topLeftRadius = 0;
|
int topLeftRadius = 0;
|
||||||
int topRightRadius = 0;
|
int topRightRadius = 0;
|
||||||
|
@ -1148,6 +1137,28 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void showStatusContent(boolean show) {
|
||||||
|
int visibility = show ? View.VISIBLE : View.GONE;
|
||||||
|
avatar.setVisibility(visibility);
|
||||||
|
avatarInset.setVisibility(visibility);
|
||||||
|
displayName.setVisibility(visibility);
|
||||||
|
username.setVisibility(visibility);
|
||||||
|
timestampInfo.setVisibility(visibility);
|
||||||
|
contentWarningDescription.setVisibility(visibility);
|
||||||
|
contentWarningButton.setVisibility(visibility);
|
||||||
|
content.setVisibility(visibility);
|
||||||
|
cardView.setVisibility(visibility);
|
||||||
|
mediaContainer.setVisibility(visibility);
|
||||||
|
pollOptions.setVisibility(visibility);
|
||||||
|
pollButton.setVisibility(visibility);
|
||||||
|
pollDescription.setVisibility(visibility);
|
||||||
|
replyButton.setVisibility(visibility);
|
||||||
|
reblogButton.setVisibility(visibility);
|
||||||
|
favouriteButton.setVisibility(visibility);
|
||||||
|
bookmarkButton.setVisibility(visibility);
|
||||||
|
moreButton.setVisibility(visibility);
|
||||||
|
}
|
||||||
|
|
||||||
private static String formatDuration(double durationInSeconds) {
|
private static String formatDuration(double durationInSeconds) {
|
||||||
int seconds = (int) Math.round(durationInSeconds) % 60;
|
int seconds = (int) Math.round(durationInSeconds) % 60;
|
||||||
int minutes = (int) durationInSeconds % 3600 / 60;
|
int minutes = (int) durationInSeconds % 3600 / 60;
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
package com.keylesspalace.tusky.adapter;
|
package com.keylesspalace.tusky.adapter;
|
||||||
|
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
@ -101,10 +99,10 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setupWithStatus(final StatusViewData.Concrete status,
|
public void setupWithStatus(@NonNull final StatusViewData.Concrete status,
|
||||||
final StatusActionListener listener,
|
@NonNull final StatusActionListener listener,
|
||||||
StatusDisplayOptions statusDisplayOptions,
|
@NonNull StatusDisplayOptions statusDisplayOptions,
|
||||||
@Nullable Object payloads) {
|
@Nullable Object payloads) {
|
||||||
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
|
super.setupWithStatus(status, listener, statusDisplayOptions, payloads);
|
||||||
setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status
|
setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status
|
||||||
if (payloads == null) {
|
if (payloads == null) {
|
||||||
|
@ -118,19 +116,6 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
|
|
||||||
setApplication(status.getActionable().getApplication());
|
setApplication(status.getActionable().getApplication());
|
||||||
|
|
||||||
View.OnLongClickListener longClickListener = view -> {
|
|
||||||
TextView textView = (TextView) view;
|
|
||||||
ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
|
||||||
ClipData clip = ClipData.newPlainText("toot", textView.getText());
|
|
||||||
clipboard.setPrimaryClip(clip);
|
|
||||||
|
|
||||||
Toast.makeText(view.getContext(), R.string.copy_to_clipboard_success, Toast.LENGTH_SHORT).show();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
content.setOnLongClickListener(longClickListener);
|
|
||||||
contentWarningDescription.setOnLongClickListener(longClickListener);
|
|
||||||
setStatusVisibility(status.getActionable().getVisibility());
|
setStatusVisibility(status.getActionable().getVisibility());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import android.view.View;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
@ -58,9 +59,9 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setupWithStatus(StatusViewData.Concrete status,
|
public void setupWithStatus(@NonNull StatusViewData.Concrete status,
|
||||||
final StatusActionListener listener,
|
@NonNull final StatusActionListener listener,
|
||||||
StatusDisplayOptions statusDisplayOptions,
|
@NonNull StatusDisplayOptions statusDisplayOptions,
|
||||||
@Nullable Object payloads) {
|
@Nullable Object payloads) {
|
||||||
if (payloads == null) {
|
if (payloads == null) {
|
||||||
|
|
||||||
|
@ -129,4 +130,9 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
||||||
content.setFilters(NO_INPUT_FILTER);
|
content.setFilters(NO_INPUT_FILTER);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void showStatusContent(boolean show) {
|
||||||
|
super.showStatusContent(show);
|
||||||
|
contentCollapseButton.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,6 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.ColorStateList
|
import android.content.res.ColorStateList
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.graphics.PorterDuff
|
|
||||||
import android.graphics.PorterDuffColorFilter
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.Editable
|
import android.text.Editable
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
|
@ -39,7 +37,6 @@ import androidx.core.view.WindowCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
import androidx.core.view.WindowInsetsCompat.Type.systemBars
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
import androidx.emoji.text.EmojiCompat
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.viewpager2.widget.MarginPageTransformer
|
import androidx.viewpager2.widget.MarginPageTransformer
|
||||||
|
@ -78,7 +75,7 @@ import com.keylesspalace.tusky.util.emojify
|
||||||
import com.keylesspalace.tusky.util.getDomain
|
import com.keylesspalace.tusky.util.getDomain
|
||||||
import com.keylesspalace.tusky.util.hide
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.loadAvatar
|
import com.keylesspalace.tusky.util.loadAvatar
|
||||||
import com.keylesspalace.tusky.util.openLink
|
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||||
import com.keylesspalace.tusky.util.setClickableText
|
import com.keylesspalace.tusky.util.setClickableText
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
|
@ -374,13 +371,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
.show()
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModel.accountFieldData.observe(
|
|
||||||
this,
|
|
||||||
{
|
|
||||||
accountFieldAdapter.fields = it
|
|
||||||
accountFieldAdapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
viewModel.noteSaved.observe(this) {
|
viewModel.noteSaved.observe(this) {
|
||||||
binding.saveNoteInfo.visible(it, View.INVISIBLE)
|
binding.saveNoteInfo.visible(it, View.INVISIBLE)
|
||||||
}
|
}
|
||||||
|
@ -395,11 +385,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
adapter.refreshContent()
|
adapter.refreshContent()
|
||||||
}
|
}
|
||||||
viewModel.isRefreshing.observe(
|
viewModel.isRefreshing.observe(
|
||||||
this,
|
this
|
||||||
{ isRefreshing ->
|
) { isRefreshing ->
|
||||||
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true
|
||||||
}
|
}
|
||||||
)
|
|
||||||
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.chinwag_green)
|
binding.swipeToRefreshLayout.setColorSchemeResources(R.color.chinwag_green)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -410,10 +399,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
binding.accountUsernameTextView.text = usernameFormatted
|
binding.accountUsernameTextView.text = usernameFormatted
|
||||||
binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis)
|
binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis)
|
||||||
|
|
||||||
val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
|
val emojifiedNote = account.note.parseAsMastodonHtml().emojify(account.emojis, binding.accountNoteTextView, animateEmojis)
|
||||||
setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this)
|
setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this)
|
||||||
|
|
||||||
// accountFieldAdapter.fields = account.fields ?: emptyList()
|
accountFieldAdapter.fields = account.fields ?: emptyList()
|
||||||
accountFieldAdapter.emojis = account.emojis ?: emptyList()
|
accountFieldAdapter.emojis = account.emojis ?: emptyList()
|
||||||
accountFieldAdapter.notifyDataSetChanged()
|
accountFieldAdapter.notifyDataSetChanged()
|
||||||
|
|
||||||
|
@ -469,14 +458,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
*/
|
*/
|
||||||
private fun updateToolbar() {
|
private fun updateToolbar() {
|
||||||
loadedAccount?.let { account ->
|
loadedAccount?.let { account ->
|
||||||
|
supportActionBar?.title = account.name.emojify(account.emojis, binding.accountToolbar, animateEmojis)
|
||||||
val emojifiedName = account.name.emojify(account.emojis, binding.accountToolbar, animateEmojis)
|
|
||||||
|
|
||||||
try {
|
|
||||||
supportActionBar?.title = EmojiCompat.get().process(emojifiedName)
|
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
supportActionBar?.title = emojifiedName
|
|
||||||
}
|
|
||||||
supportActionBar?.subtitle = String.format(getString(R.string.post_username_format), account.username)
|
supportActionBar?.subtitle = String.format(getString(R.string.post_username_format), account.username)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -501,13 +483,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
loadAvatar(movedAccount.avatar, binding.accountMovedAvatar, avatarRadius, animateAvatar)
|
loadAvatar(movedAccount.avatar, binding.accountMovedAvatar, avatarRadius, animateAvatar)
|
||||||
|
|
||||||
binding.accountMovedText.text = getString(R.string.account_moved_description, movedAccount.name)
|
binding.accountMovedText.text = getString(R.string.account_moved_description, movedAccount.name)
|
||||||
|
|
||||||
// this is necessary because API 19 can't handle vector compound drawables
|
|
||||||
val movedIcon = ContextCompat.getDrawable(this, R.drawable.ic_briefcase)?.mutate()
|
|
||||||
val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
|
||||||
movedIcon?.colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
|
|
||||||
|
|
||||||
binding.accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.account
|
package com.keylesspalace.tusky.components.account
|
||||||
|
|
||||||
import android.text.method.LinkMovementMethod
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -23,12 +22,10 @@ import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding
|
import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
import com.keylesspalace.tusky.entity.Field
|
import com.keylesspalace.tusky.entity.Field
|
||||||
import com.keylesspalace.tusky.entity.IdentityProof
|
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
import com.keylesspalace.tusky.util.Either
|
|
||||||
import com.keylesspalace.tusky.util.createClickableText
|
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||||
import com.keylesspalace.tusky.util.setClickableText
|
import com.keylesspalace.tusky.util.setClickableText
|
||||||
|
|
||||||
class AccountFieldAdapter(
|
class AccountFieldAdapter(
|
||||||
|
@ -37,7 +34,7 @@ class AccountFieldAdapter(
|
||||||
) : RecyclerView.Adapter<BindingHolder<ItemAccountFieldBinding>>() {
|
) : RecyclerView.Adapter<BindingHolder<ItemAccountFieldBinding>>() {
|
||||||
|
|
||||||
var emojis: List<Emoji> = emptyList()
|
var emojis: List<Emoji> = emptyList()
|
||||||
var fields: List<Either<IdentityProof, Field>> = emptyList()
|
var fields: List<Field> = emptyList()
|
||||||
|
|
||||||
override fun getItemCount() = fields.size
|
override fun getItemCount() = fields.size
|
||||||
|
|
||||||
|
@ -47,32 +44,20 @@ class AccountFieldAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: BindingHolder<ItemAccountFieldBinding>, position: Int) {
|
override fun onBindViewHolder(holder: BindingHolder<ItemAccountFieldBinding>, position: Int) {
|
||||||
val proofOrField = fields[position]
|
val field = fields[position]
|
||||||
val nameTextView = holder.binding.accountFieldName
|
val nameTextView = holder.binding.accountFieldName
|
||||||
val valueTextView = holder.binding.accountFieldValue
|
val valueTextView = holder.binding.accountFieldValue
|
||||||
|
|
||||||
if (proofOrField.isLeft()) {
|
val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
|
||||||
val identityProof = proofOrField.asLeft()
|
nameTextView.text = emojifiedName
|
||||||
|
|
||||||
nameTextView.text = identityProof.provider
|
val emojifiedValue = field.value.parseAsMastodonHtml().emojify(emojis, valueTextView, animateEmojis)
|
||||||
valueTextView.text = createClickableText(identityProof.username, identityProof.profileUrl)
|
setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener)
|
||||||
|
|
||||||
valueTextView.movementMethod = LinkMovementMethod.getInstance()
|
|
||||||
|
|
||||||
|
if (field.verifiedAt != null) {
|
||||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
||||||
} else {
|
} else {
|
||||||
val field = proofOrField.asRight()
|
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||||
val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis)
|
|
||||||
nameTextView.text = emojifiedName
|
|
||||||
|
|
||||||
val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis)
|
|
||||||
setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener)
|
|
||||||
|
|
||||||
if (field.verifiedAt != null) {
|
|
||||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
|
||||||
} else {
|
|
||||||
valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,17 +10,13 @@ import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
||||||
import com.keylesspalace.tusky.appstore.UnfollowEvent
|
import com.keylesspalace.tusky.appstore.UnfollowEvent
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.entity.Field
|
|
||||||
import com.keylesspalace.tusky.entity.IdentityProof
|
|
||||||
import com.keylesspalace.tusky.entity.Relationship
|
import com.keylesspalace.tusky.entity.Relationship
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.Either
|
|
||||||
import com.keylesspalace.tusky.util.Error
|
import com.keylesspalace.tusky.util.Error
|
||||||
import com.keylesspalace.tusky.util.Loading
|
import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Resource
|
import com.keylesspalace.tusky.util.Resource
|
||||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
import com.keylesspalace.tusky.util.combineOptionalLiveData
|
|
||||||
import io.reactivex.rxjava3.core.Single
|
import io.reactivex.rxjava3.core.Single
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
import io.reactivex.rxjava3.disposables.Disposable
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
|
@ -40,13 +36,6 @@ class AccountViewModel @Inject constructor(
|
||||||
|
|
||||||
val noteSaved = MutableLiveData<Boolean>()
|
val noteSaved = MutableLiveData<Boolean>()
|
||||||
|
|
||||||
private val identityProofData = MutableLiveData<List<IdentityProof>>()
|
|
||||||
|
|
||||||
val accountFieldData = combineOptionalLiveData(accountData, identityProofData) { accountRes, identityProofs ->
|
|
||||||
identityProofs.orEmpty().map { Either.Left<IdentityProof, Field>(it) }
|
|
||||||
.plus(accountRes?.data?.fields.orEmpty().map { Either.Right(it) })
|
|
||||||
}
|
|
||||||
|
|
||||||
val isRefreshing = MutableLiveData<Boolean>().apply { value = false }
|
val isRefreshing = MutableLiveData<Boolean>().apply { value = false }
|
||||||
private var isDataLoading = false
|
private var isDataLoading = false
|
||||||
|
|
||||||
|
@ -106,22 +95,6 @@ class AccountViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun obtainIdentityProof(reload: Boolean = false) {
|
|
||||||
if (identityProofData.value == null || reload) {
|
|
||||||
|
|
||||||
mastodonApi.identityProofs(accountId)
|
|
||||||
.subscribe(
|
|
||||||
{ proofs ->
|
|
||||||
identityProofData.postValue(proofs)
|
|
||||||
},
|
|
||||||
{ t ->
|
|
||||||
Log.w(TAG, "failed obtaining identity proofs", t)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.autoDispose()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun changeFollowState() {
|
fun changeFollowState() {
|
||||||
val relationship = relationshipData.value?.data
|
val relationship = relationshipData.value?.data
|
||||||
if (relationship?.following == true || relationship?.requested == true) {
|
if (relationship?.following == true || relationship?.requested == true) {
|
||||||
|
@ -314,7 +287,6 @@ class AccountViewModel @Inject constructor(
|
||||||
return
|
return
|
||||||
accountId.let {
|
accountId.let {
|
||||||
obtainAccount(isReload)
|
obtainAccount(isReload)
|
||||||
obtainIdentityProof()
|
|
||||||
if (!isSelf)
|
if (!isSelf)
|
||||||
obtainRelationship(isReload)
|
obtainRelationship(isReload)
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,7 @@ import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
import com.keylesspalace.tusky.util.BindingHolder
|
import com.keylesspalace.tusky.util.BindingHolder
|
||||||
import com.keylesspalace.tusky.util.EmojiSpan
|
import com.keylesspalace.tusky.util.EmojiSpan
|
||||||
import com.keylesspalace.tusky.util.emojify
|
import com.keylesspalace.tusky.util.emojify
|
||||||
|
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||||
import com.keylesspalace.tusky.util.setClickableText
|
import com.keylesspalace.tusky.util.setClickableText
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
|
|
||||||
|
@ -60,7 +61,7 @@ class AnnouncementAdapter(
|
||||||
val chips = holder.binding.chipGroup
|
val chips = holder.binding.chipGroup
|
||||||
val addReactionChip = holder.binding.addReactionChip
|
val addReactionChip = holder.binding.addReactionChip
|
||||||
|
|
||||||
val emojifiedText: CharSequence = item.content.emojify(item.emojis, text, animateEmojis)
|
val emojifiedText: CharSequence = item.content.parseAsMastodonHtml().emojify(item.emojis, text, animateEmojis)
|
||||||
|
|
||||||
setClickableText(text, emojifiedText, item.mentions, item.tags, listener)
|
setClickableText(text, emojifiedText, item.mentions, item.tags, listener)
|
||||||
|
|
||||||
|
|
|
@ -18,31 +18,26 @@ package com.keylesspalace.tusky.components.announcements
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
|
import com.keylesspalace.tusky.appstore.AnnouncementReadEvent
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
|
||||||
import com.keylesspalace.tusky.db.InstanceEntity
|
|
||||||
import com.keylesspalace.tusky.entity.Announcement
|
import com.keylesspalace.tusky.entity.Announcement
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
import com.keylesspalace.tusky.entity.Instance
|
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.Either
|
|
||||||
import com.keylesspalace.tusky.util.Error
|
import com.keylesspalace.tusky.util.Error
|
||||||
import com.keylesspalace.tusky.util.Loading
|
import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Resource
|
import com.keylesspalace.tusky.util.Resource
|
||||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
import io.reactivex.rxjava3.core.Single
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class AnnouncementsViewModel @Inject constructor(
|
class AnnouncementsViewModel @Inject constructor(
|
||||||
accountManager: AccountManager,
|
private val instanceInfoRepo: InstanceInfoRepository,
|
||||||
private val appDatabase: AppDatabase,
|
|
||||||
private val mastodonApi: MastodonApi,
|
private val mastodonApi: MastodonApi,
|
||||||
private val eventHub: EventHub
|
private val eventHub: EventHub
|
||||||
) : RxAwareViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val announcementsMutable = MutableLiveData<Resource<List<Announcement>>>()
|
private val announcementsMutable = MutableLiveData<Resource<List<Announcement>>>()
|
||||||
val announcements: LiveData<Resource<List<Announcement>>> = announcementsMutable
|
val announcements: LiveData<Resource<List<Announcement>>> = announcementsMutable
|
||||||
|
@ -51,155 +46,130 @@ class AnnouncementsViewModel @Inject constructor(
|
||||||
val emojis: LiveData<List<Emoji>> = emojisMutable
|
val emojis: LiveData<List<Emoji>> = emojisMutable
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Single.zip(
|
viewModelScope.launch {
|
||||||
mastodonApi.getCustomEmojis(),
|
emojisMutable.postValue(instanceInfoRepo.getEmojis())
|
||||||
appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
|
||||||
.map<Either<InstanceEntity, Instance>> { Either.Left(it) }
|
|
||||||
.onErrorResumeNext {
|
|
||||||
mastodonApi.getInstance()
|
|
||||||
.map { Either.Right(it) }
|
|
||||||
}
|
|
||||||
) { emojis, either ->
|
|
||||||
either.asLeftOrNull()?.copy(emojiList = emojis)
|
|
||||||
?: InstanceEntity(
|
|
||||||
accountManager.activeAccount?.domain!!,
|
|
||||||
emojis,
|
|
||||||
either.asRight().configuration?.statuses?.maxCharacters ?: either.asRight().maxTootChars,
|
|
||||||
either.asRight().configuration?.polls?.maxOptions ?: either.asRight().pollConfiguration?.maxOptions,
|
|
||||||
either.asRight().configuration?.polls?.maxCharactersPerOption ?: either.asRight().pollConfiguration?.maxOptionChars,
|
|
||||||
either.asRight().configuration?.polls?.minExpiration ?: either.asRight().pollConfiguration?.minExpiration,
|
|
||||||
either.asRight().configuration?.polls?.maxExpiration ?: either.asRight().pollConfiguration?.maxExpiration,
|
|
||||||
either.asRight().configuration?.statuses?.charactersReservedPerUrl ?: DEFAULT_MAXIMUM_URL_LENGTH,
|
|
||||||
either.asRight().version
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.doOnSuccess {
|
|
||||||
appDatabase.instanceDao().insertOrReplace(it)
|
|
||||||
}
|
|
||||||
.subscribe(
|
|
||||||
{
|
|
||||||
emojisMutable.postValue(it.emojiList.orEmpty())
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Log.w(TAG, "Failed to get custom emojis.", it)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.autoDispose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun load() {
|
fun load() {
|
||||||
announcementsMutable.postValue(Loading())
|
viewModelScope.launch {
|
||||||
mastodonApi.listAnnouncements()
|
announcementsMutable.postValue(Loading())
|
||||||
.subscribe(
|
mastodonApi.listAnnouncements()
|
||||||
{
|
.fold(
|
||||||
announcementsMutable.postValue(Success(it))
|
{
|
||||||
it.filter { announcement -> !announcement.read }
|
announcementsMutable.postValue(Success(it))
|
||||||
.forEach { announcement ->
|
it.filter { announcement -> !announcement.read }
|
||||||
mastodonApi.dismissAnnouncement(announcement.id)
|
.forEach { announcement ->
|
||||||
.subscribe(
|
mastodonApi.dismissAnnouncement(announcement.id)
|
||||||
{
|
.fold(
|
||||||
eventHub.dispatch(AnnouncementReadEvent(announcement.id))
|
{
|
||||||
},
|
eventHub.dispatch(AnnouncementReadEvent(announcement.id))
|
||||||
{ throwable ->
|
},
|
||||||
Log.d(TAG, "Failed to mark announcement as read.", throwable)
|
{ throwable ->
|
||||||
}
|
Log.d(
|
||||||
)
|
TAG,
|
||||||
.autoDispose()
|
"Failed to mark announcement as read.",
|
||||||
}
|
throwable
|
||||||
},
|
)
|
||||||
{
|
}
|
||||||
announcementsMutable.postValue(Error(cause = it))
|
)
|
||||||
}
|
}
|
||||||
)
|
},
|
||||||
.autoDispose()
|
{
|
||||||
|
announcementsMutable.postValue(Error(cause = it))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addReaction(announcementId: String, name: String) {
|
fun addReaction(announcementId: String, name: String) {
|
||||||
mastodonApi.addAnnouncementReaction(announcementId, name)
|
viewModelScope.launch {
|
||||||
.subscribe(
|
mastodonApi.addAnnouncementReaction(announcementId, name)
|
||||||
{
|
.fold(
|
||||||
announcementsMutable.postValue(
|
{
|
||||||
Success(
|
announcementsMutable.postValue(
|
||||||
announcements.value!!.data!!.map { announcement ->
|
Success(
|
||||||
if (announcement.id == announcementId) {
|
announcements.value!!.data!!.map { announcement ->
|
||||||
announcement.copy(
|
if (announcement.id == announcementId) {
|
||||||
reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
|
announcement.copy(
|
||||||
announcement.reactions.map { reaction ->
|
reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) {
|
||||||
|
announcement.reactions.map { reaction ->
|
||||||
|
if (reaction.name == name) {
|
||||||
|
reaction.copy(
|
||||||
|
count = reaction.count + 1,
|
||||||
|
me = true
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
reaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
listOf(
|
||||||
|
*announcement.reactions.toTypedArray(),
|
||||||
|
emojis.value!!.find { emoji -> emoji.shortcode == name }
|
||||||
|
!!.run {
|
||||||
|
Announcement.Reaction(
|
||||||
|
name,
|
||||||
|
1,
|
||||||
|
true,
|
||||||
|
url,
|
||||||
|
staticUrl
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
announcement
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Log.w(TAG, "Failed to add reaction to the announcement.", it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeReaction(announcementId: String, name: String) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
mastodonApi.removeAnnouncementReaction(announcementId, name)
|
||||||
|
.fold(
|
||||||
|
{
|
||||||
|
announcementsMutable.postValue(
|
||||||
|
Success(
|
||||||
|
announcements.value!!.data!!.map { announcement ->
|
||||||
|
if (announcement.id == announcementId) {
|
||||||
|
announcement.copy(
|
||||||
|
reactions = announcement.reactions.mapNotNull { reaction ->
|
||||||
if (reaction.name == name) {
|
if (reaction.name == name) {
|
||||||
reaction.copy(
|
if (reaction.count > 1) {
|
||||||
count = reaction.count + 1,
|
reaction.copy(
|
||||||
me = true
|
count = reaction.count - 1,
|
||||||
)
|
me = false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
reaction
|
reaction
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
)
|
||||||
listOf(
|
} else {
|
||||||
*announcement.reactions.toTypedArray(),
|
announcement
|
||||||
emojis.value!!.find { emoji -> emoji.shortcode == name }
|
}
|
||||||
!!.run {
|
|
||||||
Announcement.Reaction(
|
|
||||||
name,
|
|
||||||
1,
|
|
||||||
true,
|
|
||||||
url,
|
|
||||||
staticUrl
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
announcement
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
)
|
||||||
)
|
},
|
||||||
},
|
{
|
||||||
{
|
Log.w(TAG, "Failed to remove reaction from the announcement.", it)
|
||||||
Log.w(TAG, "Failed to add reaction to the announcement.", it)
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
.autoDispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun removeReaction(announcementId: String, name: String) {
|
|
||||||
mastodonApi.removeAnnouncementReaction(announcementId, name)
|
|
||||||
.subscribe(
|
|
||||||
{
|
|
||||||
announcementsMutable.postValue(
|
|
||||||
Success(
|
|
||||||
announcements.value!!.data!!.map { announcement ->
|
|
||||||
if (announcement.id == announcementId) {
|
|
||||||
announcement.copy(
|
|
||||||
reactions = announcement.reactions.mapNotNull { reaction ->
|
|
||||||
if (reaction.name == name) {
|
|
||||||
if (reaction.count > 1) {
|
|
||||||
reaction.copy(
|
|
||||||
count = reaction.count - 1,
|
|
||||||
me = false
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
reaction
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
announcement
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Log.w(TAG, "Failed to remove reaction from the announcement.", it)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.autoDispose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
@ -51,6 +51,8 @@ import androidx.core.view.ContentInfoCompat
|
||||||
import androidx.core.view.OnReceiveContentListener
|
import androidx.core.view.OnReceiveContentListener
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
|
@ -65,6 +67,7 @@ import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog
|
||||||
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
|
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
|
||||||
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener
|
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener
|
||||||
import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView
|
import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView
|
||||||
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
||||||
import com.keylesspalace.tusky.databinding.ActivityComposeBinding
|
import com.keylesspalace.tusky.databinding.ActivityComposeBinding
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
import com.keylesspalace.tusky.db.DraftAttachment
|
import com.keylesspalace.tusky.db.DraftAttachment
|
||||||
|
@ -93,6 +96,7 @@ import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizeDp
|
import com.mikepenz.iconics.utils.sizeDp
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
@ -123,8 +127,8 @@ class ComposeActivity :
|
||||||
private var photoUploadUri: Uri? = null
|
private var photoUploadUri: Uri? = null
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT
|
var maximumTootCharacters = InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT
|
||||||
var charactersReservedPerUrl = DEFAULT_MAXIMUM_URL_LENGTH
|
var charactersReservedPerUrl = InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL
|
||||||
|
|
||||||
private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
|
private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
|
||||||
|
|
||||||
|
@ -328,11 +332,10 @@ class ComposeActivity :
|
||||||
|
|
||||||
private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter) {
|
private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter) {
|
||||||
withLifecycleContext {
|
withLifecycleContext {
|
||||||
viewModel.instanceParams.observe { instanceData ->
|
viewModel.instanceInfo.observe { instanceData ->
|
||||||
maximumTootCharacters = instanceData.maxChars
|
maximumTootCharacters = instanceData.maxChars
|
||||||
charactersReservedPerUrl = instanceData.charactersReservedPerUrl
|
charactersReservedPerUrl = instanceData.charactersReservedPerUrl
|
||||||
updateVisibleCharactersLeft()
|
updateVisibleCharactersLeft()
|
||||||
binding.composeScheduleButton.visible(instanceData.supportsScheduled)
|
|
||||||
}
|
}
|
||||||
viewModel.emoji.observe { emoji -> setEmojiList(emoji) }
|
viewModel.emoji.observe { emoji -> setEmojiList(emoji) }
|
||||||
combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning ->
|
combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning ->
|
||||||
|
@ -342,14 +345,17 @@ class ComposeActivity :
|
||||||
viewModel.statusVisibility.observe { visibility ->
|
viewModel.statusVisibility.observe { visibility ->
|
||||||
setStatusVisibility(visibility)
|
setStatusVisibility(visibility)
|
||||||
}
|
}
|
||||||
viewModel.media.observe { media ->
|
lifecycleScope.launch {
|
||||||
mediaAdapter.submitList(media)
|
viewModel.media.collect { media ->
|
||||||
if (media.size != mediaCount) {
|
mediaAdapter.submitList(media)
|
||||||
mediaCount = media.size
|
if (media.size != mediaCount) {
|
||||||
binding.composeMediaPreviewBar.visible(media.isNotEmpty())
|
mediaCount = media.size
|
||||||
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false)
|
binding.composeMediaPreviewBar.visible(media.isNotEmpty())
|
||||||
|
updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.poll.observe { poll ->
|
viewModel.poll.observe { poll ->
|
||||||
binding.pollPreview.visible(poll != null)
|
binding.pollPreview.visible(poll != null)
|
||||||
poll?.let(binding.pollPreview::setPoll)
|
poll?.let(binding.pollPreview::setPoll)
|
||||||
|
@ -362,7 +368,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
updateScheduleButton()
|
updateScheduleButton()
|
||||||
}
|
}
|
||||||
combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll ->
|
combineOptionalLiveData(viewModel.media.asLiveData(), viewModel.poll) { media, poll ->
|
||||||
val active = poll == null &&
|
val active = poll == null &&
|
||||||
media!!.size != 4 &&
|
media!!.size != 4 &&
|
||||||
(media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
|
(media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE)
|
||||||
|
@ -666,7 +672,7 @@ class ComposeActivity :
|
||||||
|
|
||||||
private fun openPollDialog() {
|
private fun openPollDialog() {
|
||||||
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
val instanceParams = viewModel.instanceParams.value!!
|
val instanceParams = viewModel.instanceInfo.value!!
|
||||||
showAddPollDialog(
|
showAddPollDialog(
|
||||||
this, viewModel.poll.value, instanceParams.pollMaxOptions,
|
this, viewModel.poll.value, instanceParams.pollMaxOptions,
|
||||||
instanceParams.pollMaxLength, instanceParams.pollMinDuration, instanceParams.pollMaxDuration,
|
instanceParams.pollMaxLength, instanceParams.pollMinDuration, instanceParams.pollMaxDuration,
|
||||||
|
@ -779,11 +785,11 @@ class ComposeActivity :
|
||||||
spoilerText = binding.composeContentWarningField.text.toString()
|
spoilerText = binding.composeContentWarningField.text.toString()
|
||||||
}
|
}
|
||||||
val characterCount = calculateTextLength()
|
val characterCount = calculateTextLength()
|
||||||
if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value!!.isEmpty()) {
|
if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value.isEmpty()) {
|
||||||
binding.composeEditField.error = getString(R.string.error_empty)
|
binding.composeEditField.error = getString(R.string.error_empty)
|
||||||
enableButtons(true)
|
enableButtons(true)
|
||||||
} else if (characterCount <= maximumTootCharacters) {
|
} else if (characterCount <= maximumTootCharacters) {
|
||||||
if (viewModel.media.value!!.isNotEmpty()) {
|
if (viewModel.media.value.isNotEmpty()) {
|
||||||
finishingUploadDialog = ProgressDialog.show(
|
finishingUploadDialog = ProgressDialog.show(
|
||||||
this, getString(R.string.dialog_title_finishing_media_upload),
|
this, getString(R.string.dialog_title_finishing_media_upload),
|
||||||
getString(R.string.dialog_message_uploading_media), true, true
|
getString(R.string.dialog_message_uploading_media), true, true
|
||||||
|
@ -866,25 +872,15 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun pickMedia(uri: Uri) {
|
private fun pickMedia(uri: Uri) {
|
||||||
withLifecycleContext {
|
lifecycleScope.launch {
|
||||||
viewModel.pickMedia(uri).observe { exceptionOrItem ->
|
viewModel.pickMedia(uri).onFailure { throwable ->
|
||||||
exceptionOrItem.asLeftOrNull()?.let {
|
val errorId = when (throwable) {
|
||||||
val errorId = when (it) {
|
is VideoSizeException -> R.string.error_video_upload_size
|
||||||
is VideoSizeException -> {
|
is AudioSizeException -> R.string.error_audio_upload_size
|
||||||
R.string.error_video_upload_size
|
is VideoOrImageException -> R.string.error_media_upload_image_or_video
|
||||||
}
|
else -> R.string.error_media_upload_opening
|
||||||
is AudioSizeException -> {
|
|
||||||
R.string.error_audio_upload_size
|
|
||||||
}
|
|
||||||
is VideoOrImageException -> {
|
|
||||||
R.string.error_media_upload_image_or_video
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
R.string.error_media_upload_opening
|
|
||||||
}
|
|
||||||
}
|
|
||||||
displayTransientError(errorId)
|
|
||||||
}
|
}
|
||||||
|
displayTransientError(errorId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -971,8 +967,19 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveDraftAndFinish(contentText: String, contentWarning: String) {
|
private fun saveDraftAndFinish(contentText: String, contentWarning: String) {
|
||||||
viewModel.saveDraft(contentText, contentWarning)
|
lifecycleScope.launch {
|
||||||
finishWithoutSlideOutAnimation()
|
val dialog = if (viewModel.shouldShowSaveDraftDialog()) {
|
||||||
|
ProgressDialog.show(
|
||||||
|
this@ComposeActivity, null,
|
||||||
|
getString(R.string.saving_draft), true, false
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
viewModel.saveDraft(contentText, contentWarning)
|
||||||
|
dialog?.cancel()
|
||||||
|
finishWithoutSlideOutAnimation()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
override fun search(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
||||||
|
@ -991,7 +998,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
|
|
||||||
data class QueuedMedia(
|
data class QueuedMedia(
|
||||||
val localId: Long,
|
val localId: Int,
|
||||||
val uri: Uri,
|
val uri: Uri,
|
||||||
val type: Type,
|
val type: Type,
|
||||||
val mediaSize: Long,
|
val mediaSize: Long,
|
||||||
|
|
|
@ -20,14 +20,15 @@ import android.util.Log
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||||
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||||
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo
|
||||||
|
import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository
|
||||||
import com.keylesspalace.tusky.components.search.SearchType
|
import com.keylesspalace.tusky.components.search.SearchType
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
|
||||||
import com.keylesspalace.tusky.db.InstanceEntity
|
|
||||||
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.NewPoll
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
|
@ -35,19 +36,21 @@ import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.service.ServiceClient
|
import com.keylesspalace.tusky.service.ServiceClient
|
||||||
import com.keylesspalace.tusky.service.StatusToSend
|
import com.keylesspalace.tusky.service.StatusToSend
|
||||||
import com.keylesspalace.tusky.util.Either
|
|
||||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
|
||||||
import com.keylesspalace.tusky.util.VersionUtils
|
|
||||||
import com.keylesspalace.tusky.util.combineLiveData
|
import com.keylesspalace.tusky.util.combineLiveData
|
||||||
import com.keylesspalace.tusky.util.filter
|
|
||||||
import com.keylesspalace.tusky.util.map
|
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
import com.keylesspalace.tusky.util.toLiveData
|
import com.keylesspalace.tusky.util.toLiveData
|
||||||
import com.keylesspalace.tusky.util.withoutFirstWhich
|
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
import io.reactivex.rxjava3.core.Single
|
import kotlinx.coroutines.Dispatchers
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.flow.updateAndGet
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.rx3.rxSingle
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -57,8 +60,8 @@ class ComposeViewModel @Inject constructor(
|
||||||
private val mediaUploader: MediaUploader,
|
private val mediaUploader: MediaUploader,
|
||||||
private val serviceClient: ServiceClient,
|
private val serviceClient: ServiceClient,
|
||||||
private val draftHelper: DraftHelper,
|
private val draftHelper: DraftHelper,
|
||||||
private val db: AppDatabase
|
private val instanceInfoRepo: InstanceInfoRepository
|
||||||
) : RxAwareViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private var replyingStatusAuthor: String? = null
|
private var replyingStatusAuthor: String? = null
|
||||||
private var replyingStatusContent: String? = null
|
private var replyingStatusContent: String? = null
|
||||||
|
@ -72,19 +75,8 @@ class ComposeViewModel @Inject constructor(
|
||||||
private var contentWarningStateChanged: Boolean = false
|
private var contentWarningStateChanged: Boolean = false
|
||||||
private var modifiedInitialState: Boolean = false
|
private var modifiedInitialState: Boolean = false
|
||||||
|
|
||||||
private val instance: MutableLiveData<InstanceEntity?> = MutableLiveData(null)
|
val instanceInfo: MutableLiveData<InstanceInfo> = MutableLiveData()
|
||||||
|
|
||||||
val instanceParams: LiveData<ComposeInstanceParams> = instance.map { instance ->
|
|
||||||
ComposeInstanceParams(
|
|
||||||
maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT,
|
|
||||||
pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT,
|
|
||||||
pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
|
|
||||||
pollMinDuration = instance?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION,
|
|
||||||
pollMaxDuration = instance?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION,
|
|
||||||
charactersReservedPerUrl = instance?.charactersReservedPerUrl ?: DEFAULT_MAXIMUM_URL_LENGTH,
|
|
||||||
supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val emoji: MutableLiveData<List<Emoji>?> = MutableLiveData()
|
val emoji: MutableLiveData<List<Emoji>?> = MutableLiveData()
|
||||||
val markMediaAsSensitive =
|
val markMediaAsSensitive =
|
||||||
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
||||||
|
@ -95,131 +87,104 @@ class ComposeViewModel @Inject constructor(
|
||||||
val poll: MutableLiveData<NewPoll?> = mutableLiveData(null)
|
val poll: MutableLiveData<NewPoll?> = mutableLiveData(null)
|
||||||
val scheduledAt: MutableLiveData<String?> = mutableLiveData(null)
|
val scheduledAt: MutableLiveData<String?> = mutableLiveData(null)
|
||||||
|
|
||||||
val media = mutableLiveData<List<QueuedMedia>>(listOf())
|
val media: MutableStateFlow<List<QueuedMedia>> = MutableStateFlow(emptyList())
|
||||||
val uploadError = MutableLiveData<Throwable>()
|
val uploadError = MutableLiveData<Throwable>()
|
||||||
|
|
||||||
private val mediaToDisposable = mutableMapOf<Long, Disposable>()
|
private val mediaToJob = mutableMapOf<Int, Job>()
|
||||||
|
|
||||||
private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty()
|
private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
Single.zip(
|
emoji.postValue(instanceInfoRepo.getEmojis())
|
||||||
api.getCustomEmojis(), api.getInstance()
|
}
|
||||||
) { emojis, instance ->
|
viewModelScope.launch {
|
||||||
InstanceEntity(
|
instanceInfo.postValue(instanceInfoRepo.getInstanceInfo())
|
||||||
instance = accountManager.activeAccount?.domain!!,
|
|
||||||
emojiList = emojis,
|
|
||||||
maximumTootCharacters = instance.configuration?.statuses?.maxCharacters ?: instance.maxTootChars,
|
|
||||||
maxPollOptions = instance.configuration?.polls?.maxOptions ?: instance.pollConfiguration?.maxOptions,
|
|
||||||
maxPollOptionLength = instance.configuration?.polls?.maxCharactersPerOption ?: instance.pollConfiguration?.maxOptionChars,
|
|
||||||
minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration,
|
|
||||||
maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration,
|
|
||||||
charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl,
|
|
||||||
version = instance.version
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.doOnSuccess {
|
|
||||||
db.instanceDao().insertOrReplace(it)
|
|
||||||
}
|
|
||||||
.onErrorResumeNext {
|
|
||||||
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
|
||||||
}
|
|
||||||
.subscribe(
|
|
||||||
{ instanceEntity ->
|
|
||||||
emoji.postValue(instanceEntity.emojiList)
|
|
||||||
instance.postValue(instanceEntity)
|
|
||||||
},
|
|
||||||
{ throwable ->
|
|
||||||
// this can happen on network error when no cached data is available
|
|
||||||
Log.w(TAG, "error loading instance data", throwable)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.autoDispose()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pickMedia(uri: Uri, description: String? = null): LiveData<Either<Throwable, QueuedMedia>> {
|
suspend fun pickMedia(mediaUri: Uri, description: String? = null): Result<QueuedMedia> = withContext(Dispatchers.IO) {
|
||||||
// We are not calling .toLiveData() here because we don't want to stop the process when
|
try {
|
||||||
// the Activity goes away temporarily (like on screen rotation).
|
val (type, uri, size) = mediaUploader.prepareMedia(mediaUri)
|
||||||
val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>()
|
val mediaItems = media.value
|
||||||
mediaUploader.prepareMedia(uri)
|
if (type != QueuedMedia.Type.IMAGE &&
|
||||||
.map { (type, uri, size) ->
|
mediaItems.isNotEmpty() &&
|
||||||
val mediaItems = media.value!!
|
mediaItems[0].type == QueuedMedia.Type.IMAGE
|
||||||
if (type != QueuedMedia.Type.IMAGE &&
|
) {
|
||||||
mediaItems.isNotEmpty() &&
|
Result.failure(VideoOrImageException())
|
||||||
mediaItems[0].type == QueuedMedia.Type.IMAGE
|
} else {
|
||||||
) {
|
val queuedMedia = addMediaToQueue(type, uri, size, description)
|
||||||
throw VideoOrImageException()
|
Result.success(queuedMedia)
|
||||||
} else {
|
|
||||||
addMediaToQueue(type, uri, size, description)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.subscribe(
|
} catch (e: Exception) {
|
||||||
{ queuedMedia ->
|
Result.failure(e)
|
||||||
liveData.postValue(Either.Right(queuedMedia))
|
}
|
||||||
},
|
|
||||||
{ error ->
|
|
||||||
liveData.postValue(Either.Left(error))
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.autoDispose()
|
|
||||||
return liveData
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addMediaToQueue(
|
private suspend fun addMediaToQueue(
|
||||||
type: QueuedMedia.Type,
|
type: QueuedMedia.Type,
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
mediaSize: Long,
|
mediaSize: Long,
|
||||||
description: String? = null
|
description: String? = null
|
||||||
): QueuedMedia {
|
): QueuedMedia {
|
||||||
val mediaItem = QueuedMedia(
|
val mediaItem = media.updateAndGet { mediaValue ->
|
||||||
localId = System.currentTimeMillis(),
|
val mediaItem = QueuedMedia(
|
||||||
uri = uri,
|
localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1,
|
||||||
type = type,
|
uri = uri,
|
||||||
mediaSize = mediaSize,
|
type = type,
|
||||||
description = description
|
mediaSize = mediaSize,
|
||||||
)
|
description = description
|
||||||
media.value = media.value!! + mediaItem
|
)
|
||||||
mediaToDisposable[mediaItem.localId] = mediaUploader
|
mediaValue + mediaItem
|
||||||
.uploadMedia(mediaItem)
|
}.last()
|
||||||
.subscribe(
|
mediaToJob[mediaItem.localId] = viewModelScope.launch {
|
||||||
{ event ->
|
mediaUploader
|
||||||
val item = media.value?.find { it.localId == mediaItem.localId }
|
.uploadMedia(mediaItem)
|
||||||
?: return@subscribe
|
.catch { error ->
|
||||||
|
media.update { mediaValue -> mediaValue.filter { it.localId != mediaItem.localId } }
|
||||||
|
uploadError.postValue(error)
|
||||||
|
}
|
||||||
|
.collect { event ->
|
||||||
|
val item = media.value.find { it.localId == mediaItem.localId }
|
||||||
|
?: return@collect
|
||||||
val newMediaItem = when (event) {
|
val newMediaItem = when (event) {
|
||||||
is UploadEvent.ProgressEvent ->
|
is UploadEvent.ProgressEvent ->
|
||||||
item.copy(uploadPercent = event.percentage)
|
item.copy(uploadPercent = event.percentage)
|
||||||
is UploadEvent.FinishedEvent ->
|
is UploadEvent.FinishedEvent ->
|
||||||
item.copy(id = event.mediaId, uploadPercent = -1)
|
item.copy(id = event.mediaId, uploadPercent = -1)
|
||||||
}
|
}
|
||||||
synchronized(media) {
|
media.update { mediaValue ->
|
||||||
val mediaValue = media.value!!
|
mediaValue.map { mediaItem ->
|
||||||
val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId }
|
if (mediaItem.localId == newMediaItem.localId) {
|
||||||
media.postValue(
|
newMediaItem
|
||||||
if (index == -1) {
|
|
||||||
mediaValue + newMediaItem
|
|
||||||
} else {
|
} else {
|
||||||
mediaValue.toMutableList().also { it[index] = newMediaItem }
|
mediaItem
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{ error ->
|
|
||||||
media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList())
|
|
||||||
uploadError.postValue(error)
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
return mediaItem
|
return mediaItem
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?) {
|
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?) {
|
||||||
val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, 0, -1, id, description)
|
media.update { mediaValue ->
|
||||||
media.value = media.value!! + mediaItem
|
val mediaItem = QueuedMedia(
|
||||||
|
localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1,
|
||||||
|
uri = uri,
|
||||||
|
type = type,
|
||||||
|
mediaSize = 0,
|
||||||
|
uploadPercent = -1,
|
||||||
|
id = id,
|
||||||
|
description = description
|
||||||
|
)
|
||||||
|
mediaValue + mediaItem
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeMediaFromQueue(item: QueuedMedia) {
|
fun removeMediaFromQueue(item: QueuedMedia) {
|
||||||
mediaToDisposable[item.localId]?.dispose()
|
mediaToJob[item.localId]?.cancel()
|
||||||
media.value = media.value!!.withoutFirstWhich { it.localId == item.localId }
|
media.update { mediaValue -> mediaValue.filter { it.localId != item.localId } }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleMarkSensitive() {
|
fun toggleMarkSensitive() {
|
||||||
|
@ -255,31 +220,36 @@ class ComposeViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveDraft(content: String, contentWarning: String) {
|
fun shouldShowSaveDraftDialog(): Boolean {
|
||||||
viewModelScope.launch {
|
// if any of the media files need to be downloaded first it could take a while, so show a loading dialog
|
||||||
val mediaUris: MutableList<String> = mutableListOf()
|
return media.value.any { mediaValue ->
|
||||||
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
mediaValue.uri.scheme == "https"
|
||||||
media.value?.forEach { item ->
|
|
||||||
mediaUris.add(item.uri.toString())
|
|
||||||
mediaDescriptions.add(item.description)
|
|
||||||
}
|
|
||||||
|
|
||||||
draftHelper.saveDraft(
|
|
||||||
draftId = draftId,
|
|
||||||
accountId = accountManager.activeAccount?.id!!,
|
|
||||||
inReplyToId = inReplyToId,
|
|
||||||
content = content,
|
|
||||||
contentWarning = contentWarning,
|
|
||||||
sensitive = markMediaAsSensitive.value!!,
|
|
||||||
visibility = statusVisibility.value!!,
|
|
||||||
mediaUris = mediaUris,
|
|
||||||
mediaDescriptions = mediaDescriptions,
|
|
||||||
poll = poll.value,
|
|
||||||
failedToSend = false
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun saveDraft(content: String, contentWarning: String) {
|
||||||
|
val mediaUris: MutableList<String> = mutableListOf()
|
||||||
|
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
||||||
|
media.value.forEach { item ->
|
||||||
|
mediaUris.add(item.uri.toString())
|
||||||
|
mediaDescriptions.add(item.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
draftHelper.saveDraft(
|
||||||
|
draftId = draftId,
|
||||||
|
accountId = accountManager.activeAccount?.id!!,
|
||||||
|
inReplyToId = inReplyToId,
|
||||||
|
content = content,
|
||||||
|
contentWarning = contentWarning,
|
||||||
|
sensitive = markMediaAsSensitive.value!!,
|
||||||
|
visibility = statusVisibility.value!!,
|
||||||
|
mediaUris = mediaUris,
|
||||||
|
mediaDescriptions = mediaDescriptions,
|
||||||
|
poll = poll.value,
|
||||||
|
failedToSend = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send status to the server.
|
* Send status to the server.
|
||||||
* Uses current state plus provided arguments.
|
* Uses current state plus provided arguments.
|
||||||
|
@ -291,21 +261,23 @@ class ComposeViewModel @Inject constructor(
|
||||||
): LiveData<Unit> {
|
): LiveData<Unit> {
|
||||||
|
|
||||||
val deletionObservable = if (isEditingScheduledToot) {
|
val deletionObservable = if (isEditingScheduledToot) {
|
||||||
api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { }
|
rxSingle { api.deleteScheduledStatus(scheduledTootId.toString()) }.toObservable().map { }
|
||||||
} else {
|
} else {
|
||||||
Observable.just(Unit)
|
Observable.just(Unit)
|
||||||
}.toLiveData()
|
}.toLiveData()
|
||||||
|
|
||||||
val sendObservable = media
|
val sendFlow = media
|
||||||
.filter { items -> items.all { it.uploadPercent == -1 } }
|
.filter { items -> items.all { it.uploadPercent == -1 } }
|
||||||
.map {
|
.map {
|
||||||
val mediaIds = ArrayList<String>()
|
val mediaIds: MutableList<String> = mutableListOf()
|
||||||
val mediaUris = ArrayList<Uri>()
|
val mediaUris: MutableList<Uri> = mutableListOf()
|
||||||
val mediaDescriptions = ArrayList<String>()
|
val mediaDescriptions: MutableList<String> = mutableListOf()
|
||||||
for (item in media.value!!) {
|
val mediaProcessed: MutableList<Boolean> = mutableListOf()
|
||||||
|
for (item in media.value) {
|
||||||
mediaIds.add(item.id!!)
|
mediaIds.add(item.id!!)
|
||||||
mediaUris.add(item.uri)
|
mediaUris.add(item.uri)
|
||||||
mediaDescriptions.add(item.description ?: "")
|
mediaDescriptions.add(item.description ?: "")
|
||||||
|
mediaProcessed.add(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
val tootToSend = StatusToSend(
|
val tootToSend = StatusToSend(
|
||||||
|
@ -324,44 +296,38 @@ class ComposeViewModel @Inject constructor(
|
||||||
accountId = accountManager.activeAccount!!.id,
|
accountId = accountManager.activeAccount!!.id,
|
||||||
draftId = draftId,
|
draftId = draftId,
|
||||||
idempotencyKey = randomAlphanumericString(16),
|
idempotencyKey = randomAlphanumericString(16),
|
||||||
retries = 0
|
retries = 0,
|
||||||
|
mediaProcessed = mediaProcessed
|
||||||
)
|
)
|
||||||
|
|
||||||
serviceClient.sendToot(tootToSend)
|
serviceClient.sendToot(tootToSend)
|
||||||
}
|
}
|
||||||
|
|
||||||
return combineLiveData(deletionObservable, sendObservable) { _, _ -> }
|
return combineLiveData(deletionObservable, sendFlow.asLiveData()) { _, _ -> }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateDescription(localId: Long, description: String): LiveData<Boolean> {
|
suspend fun updateDescription(localId: Int, description: String): Boolean {
|
||||||
val newList = media.value!!.toMutableList()
|
val newMediaList = media.updateAndGet { mediaValue ->
|
||||||
val index = newList.indexOfFirst { it.localId == localId }
|
mediaValue.map { mediaItem ->
|
||||||
if (index != -1) {
|
if (mediaItem.localId == localId) {
|
||||||
newList[index] = newList[index].copy(description = description)
|
mediaItem.copy(description = description)
|
||||||
}
|
} else {
|
||||||
media.value = newList
|
mediaItem
|
||||||
val completedCaptioningLiveData = MutableLiveData<Boolean>()
|
|
||||||
media.observeForever(object : Observer<List<QueuedMedia>> {
|
|
||||||
override fun onChanged(mediaItems: List<QueuedMedia>) {
|
|
||||||
val updatedItem = mediaItems.find { it.localId == localId }
|
|
||||||
if (updatedItem == null) {
|
|
||||||
media.removeObserver(this)
|
|
||||||
} else if (updatedItem.id != null) {
|
|
||||||
api.updateMedia(updatedItem.id, description)
|
|
||||||
.subscribe(
|
|
||||||
{
|
|
||||||
completedCaptioningLiveData.postValue(true)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
completedCaptioningLiveData.postValue(false)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.autoDispose()
|
|
||||||
media.removeObserver(this)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
return completedCaptioningLiveData
|
|
||||||
|
val updatedItem = newMediaList.find { it.localId == localId }
|
||||||
|
if (updatedItem?.id != null) {
|
||||||
|
return api.updateMedia(updatedItem.id, description)
|
||||||
|
.fold({
|
||||||
|
true
|
||||||
|
}, { throwable ->
|
||||||
|
Log.w(TAG, "failed to update media", throwable)
|
||||||
|
false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
||||||
|
@ -443,7 +409,11 @@ class ComposeViewModel @Inject constructor(
|
||||||
val draftAttachments = composeOptions?.draftAttachments
|
val draftAttachments = composeOptions?.draftAttachments
|
||||||
if (draftAttachments != null) {
|
if (draftAttachments != null) {
|
||||||
// when coming from DraftActivity
|
// when coming from DraftActivity
|
||||||
draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) }
|
viewModelScope.launch {
|
||||||
|
draftAttachments.forEach { attachment ->
|
||||||
|
pickMedia(attachment.uri, attachment.description)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else composeOptions?.mediaAttachments?.forEach { a ->
|
} else composeOptions?.mediaAttachments?.forEach { a ->
|
||||||
// when coming from redraft or ScheduledTootActivity
|
// when coming from redraft or ScheduledTootActivity
|
||||||
val mediaType = when (a.type) {
|
val mediaType = when (a.type) {
|
||||||
|
@ -494,13 +464,6 @@ class ComposeViewModel @Inject constructor(
|
||||||
scheduledAt.value = newScheduledAt
|
scheduledAt.value = newScheduledAt
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
for (uploadDisposable in mediaToDisposable.values) {
|
|
||||||
uploadDisposable.dispose()
|
|
||||||
}
|
|
||||||
super.onCleared()
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val TAG = "ComposeViewModel"
|
const val TAG = "ComposeViewModel"
|
||||||
}
|
}
|
||||||
|
@ -508,25 +471,6 @@ class ComposeViewModel @Inject constructor(
|
||||||
|
|
||||||
fun <T> mutableLiveData(default: T) = MutableLiveData<T>().apply { value = default }
|
fun <T> mutableLiveData(default: T) = MutableLiveData<T>().apply { value = default }
|
||||||
|
|
||||||
const val DEFAULT_CHARACTER_LIMIT = 500
|
|
||||||
private const val DEFAULT_MAX_OPTION_COUNT = 4
|
|
||||||
private const val DEFAULT_MAX_OPTION_LENGTH = 50
|
|
||||||
private const val DEFAULT_MIN_POLL_DURATION = 300
|
|
||||||
private const val DEFAULT_MAX_POLL_DURATION = 604800
|
|
||||||
|
|
||||||
// Mastodon only counts URLs as this long in terms of status character limits
|
|
||||||
const val DEFAULT_MAXIMUM_URL_LENGTH = 23
|
|
||||||
|
|
||||||
data class ComposeInstanceParams(
|
|
||||||
val maxChars: Int,
|
|
||||||
val pollMaxOptions: Int,
|
|
||||||
val pollMaxLength: Int,
|
|
||||||
val pollMinDuration: Int,
|
|
||||||
val pollMaxDuration: Int,
|
|
||||||
val charactersReservedPerUrl: Int,
|
|
||||||
val supportsScheduled: Boolean
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Thrown when trying to add an image when video is already present or the other way around
|
* Thrown when trying to add an image when video is already present or the other way around
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,154 +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.components.compose;
|
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.util.IOUtils;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import static com.keylesspalace.tusky.util.MediaUtilsKt.calculateInSampleSize;
|
|
||||||
import static com.keylesspalace.tusky.util.MediaUtilsKt.getImageOrientation;
|
|
||||||
import static com.keylesspalace.tusky.util.MediaUtilsKt.reorientBitmap;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reduces the file size of images to fit under a given limit by resizing them, maintaining both
|
|
||||||
* aspect ratio and orientation.
|
|
||||||
*/
|
|
||||||
public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
|
||||||
private int sizeLimit;
|
|
||||||
private ContentResolver contentResolver;
|
|
||||||
private Listener listener;
|
|
||||||
private File tempFile;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param sizeLimit the maximum number of bytes each image can take
|
|
||||||
* @param contentResolver to resolve the specified images' URIs
|
|
||||||
* @param tempFile the file where the result will be stored
|
|
||||||
* @param listener to whom the results are given
|
|
||||||
*/
|
|
||||||
public DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, File tempFile, Listener listener) {
|
|
||||||
this.sizeLimit = sizeLimit;
|
|
||||||
this.contentResolver = contentResolver;
|
|
||||||
this.tempFile = tempFile;
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Boolean doInBackground(Uri... uris) {
|
|
||||||
boolean result = DownsizeImageTask.resize(uris, sizeLimit, contentResolver, tempFile);
|
|
||||||
if (isCancelled()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Boolean successful) {
|
|
||||||
if (successful) {
|
|
||||||
listener.onSuccess(tempFile);
|
|
||||||
} else {
|
|
||||||
listener.onFailure();
|
|
||||||
}
|
|
||||||
super.onPostExecute(successful);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean resize(Uri[] uris, int sizeLimit, ContentResolver contentResolver,
|
|
||||||
File tempFile) {
|
|
||||||
for (Uri uri : uris) {
|
|
||||||
InputStream inputStream;
|
|
||||||
try {
|
|
||||||
inputStream = contentResolver.openInputStream(uri);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// Initially, just get the image dimensions.
|
|
||||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
|
||||||
options.inJustDecodeBounds = true;
|
|
||||||
BitmapFactory.decodeStream(inputStream, null, options);
|
|
||||||
IOUtils.closeQuietly(inputStream);
|
|
||||||
// Get EXIF data, for orientation info.
|
|
||||||
int orientation = getImageOrientation(uri, contentResolver);
|
|
||||||
/* Unfortunately, there isn't a determined worst case compression ratio for image
|
|
||||||
* formats. So, the only way to tell if they're too big is to compress them and
|
|
||||||
* test, and keep trying at smaller sizes. The initial estimate should be good for
|
|
||||||
* many cases, so it should only iterate once, but the loop is used to be absolutely
|
|
||||||
* sure it gets downsized to below the limit. */
|
|
||||||
int scaledImageSize = 1024;
|
|
||||||
do {
|
|
||||||
OutputStream stream;
|
|
||||||
try {
|
|
||||||
stream = new FileOutputStream(tempFile);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
inputStream = contentResolver.openInputStream(uri);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
options.inSampleSize = calculateInSampleSize(options, scaledImageSize, scaledImageSize);
|
|
||||||
options.inJustDecodeBounds = false;
|
|
||||||
Bitmap scaledBitmap;
|
|
||||||
try {
|
|
||||||
scaledBitmap = BitmapFactory.decodeStream(inputStream, null, options);
|
|
||||||
} catch (OutOfMemoryError error) {
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
IOUtils.closeQuietly(inputStream);
|
|
||||||
}
|
|
||||||
if (scaledBitmap == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Bitmap reorientedBitmap = reorientBitmap(scaledBitmap, orientation);
|
|
||||||
if (reorientedBitmap == null) {
|
|
||||||
scaledBitmap.recycle();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Bitmap.CompressFormat format;
|
|
||||||
/* It's not likely the user will give transparent images over the upload limit, but
|
|
||||||
* if they do, make sure the transparency is retained. */
|
|
||||||
if (!reorientedBitmap.hasAlpha()) {
|
|
||||||
format = Bitmap.CompressFormat.JPEG;
|
|
||||||
} else {
|
|
||||||
format = Bitmap.CompressFormat.PNG;
|
|
||||||
}
|
|
||||||
reorientedBitmap.compress(format, 85, stream);
|
|
||||||
reorientedBitmap.recycle();
|
|
||||||
scaledImageSize /= 2;
|
|
||||||
} while (tempFile.length() > sizeLimit);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used to communicate the results of the task.
|
|
||||||
*/
|
|
||||||
public interface Listener {
|
|
||||||
void onSuccess(File file);
|
|
||||||
|
|
||||||
void onFailure();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,101 @@
|
||||||
|
/* Copyright 2022 Tusky contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.components.compose
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.Bitmap.CompressFormat
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.net.Uri
|
||||||
|
import com.keylesspalace.tusky.util.IOUtils
|
||||||
|
import com.keylesspalace.tusky.util.calculateInSampleSize
|
||||||
|
import com.keylesspalace.tusky.util.getImageOrientation
|
||||||
|
import com.keylesspalace.tusky.util.reorientBitmap
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param uri the uri pointing to the input file
|
||||||
|
* @param sizeLimit the maximum number of bytes the output image is allowed to have
|
||||||
|
* @param contentResolver to resolve the specified input uri
|
||||||
|
* @param tempFile the file where the result will be stored
|
||||||
|
* @return true when the image was successfully resized, false otherwise
|
||||||
|
*/
|
||||||
|
fun downsizeImage(
|
||||||
|
uri: Uri,
|
||||||
|
sizeLimit: Int,
|
||||||
|
contentResolver: ContentResolver,
|
||||||
|
tempFile: File
|
||||||
|
): Boolean {
|
||||||
|
|
||||||
|
val decodeBoundsInputStream = try {
|
||||||
|
contentResolver.openInputStream(uri)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Initially, just get the image dimensions.
|
||||||
|
val options = BitmapFactory.Options()
|
||||||
|
options.inJustDecodeBounds = true
|
||||||
|
BitmapFactory.decodeStream(decodeBoundsInputStream, null, options)
|
||||||
|
IOUtils.closeQuietly(decodeBoundsInputStream)
|
||||||
|
// Get EXIF data, for orientation info.
|
||||||
|
val orientation = getImageOrientation(uri, contentResolver)
|
||||||
|
/* Unfortunately, there isn't a determined worst case compression ratio for image
|
||||||
|
* formats. So, the only way to tell if they're too big is to compress them and
|
||||||
|
* test, and keep trying at smaller sizes. The initial estimate should be good for
|
||||||
|
* many cases, so it should only iterate once, but the loop is used to be absolutely
|
||||||
|
* sure it gets downsized to below the limit. */
|
||||||
|
var scaledImageSize = 1024
|
||||||
|
do {
|
||||||
|
val outputStream = try {
|
||||||
|
FileOutputStream(tempFile)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val decodeBitmapInputStream = try {
|
||||||
|
contentResolver.openInputStream(uri)
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
options.inSampleSize = calculateInSampleSize(options, scaledImageSize, scaledImageSize)
|
||||||
|
options.inJustDecodeBounds = false
|
||||||
|
val scaledBitmap: Bitmap = try {
|
||||||
|
BitmapFactory.decodeStream(decodeBitmapInputStream, null, options)
|
||||||
|
} catch (error: OutOfMemoryError) {
|
||||||
|
return false
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(decodeBitmapInputStream)
|
||||||
|
} ?: return false
|
||||||
|
|
||||||
|
val reorientedBitmap = reorientBitmap(scaledBitmap, orientation)
|
||||||
|
if (reorientedBitmap == null) {
|
||||||
|
scaledBitmap.recycle()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
/* Retain transparency if there is any by encoding as png */
|
||||||
|
val format: CompressFormat = if (!reorientedBitmap.hasAlpha()) {
|
||||||
|
CompressFormat.JPEG
|
||||||
|
} else {
|
||||||
|
CompressFormat.PNG
|
||||||
|
}
|
||||||
|
reorientedBitmap.compress(format, 85, outputStream)
|
||||||
|
reorientedBitmap.recycle()
|
||||||
|
scaledImageSize /= 2
|
||||||
|
} while (tempFile.length() > sizeLimit)
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
|
@ -26,15 +26,20 @@ import androidx.core.net.toUri
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MediaUploadApi
|
||||||
import com.keylesspalace.tusky.network.ProgressRequestBody
|
import com.keylesspalace.tusky.network.ProgressRequestBody
|
||||||
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN
|
||||||
import com.keylesspalace.tusky.util.getImageSquarePixels
|
import com.keylesspalace.tusky.util.getImageSquarePixels
|
||||||
import com.keylesspalace.tusky.util.getMediaSize
|
import com.keylesspalace.tusky.util.getMediaSize
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import kotlinx.coroutines.Dispatchers
|
||||||
import io.reactivex.rxjava3.core.Single
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -70,63 +75,42 @@ class CouldNotOpenFileException : Exception()
|
||||||
|
|
||||||
class MediaUploader @Inject constructor(
|
class MediaUploader @Inject constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val mastodonApi: MastodonApi
|
private val mediaUploadApi: MediaUploadApi
|
||||||
) {
|
) {
|
||||||
fun uploadMedia(media: QueuedMedia): Observable<UploadEvent> {
|
|
||||||
return Observable
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
.fromCallable {
|
fun uploadMedia(media: QueuedMedia): Flow<UploadEvent> {
|
||||||
if (shouldResizeMedia(media)) {
|
return flow {
|
||||||
downsize(media)
|
if (shouldResizeMedia(media)) {
|
||||||
} else media
|
emit(downsize(media))
|
||||||
|
} else {
|
||||||
|
emit(media)
|
||||||
}
|
}
|
||||||
.switchMap { upload(it) }
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.flatMapLatest { upload(it) }
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun prepareMedia(inUri: Uri): Single<PreparedMedia> {
|
fun prepareMedia(inUri: Uri): PreparedMedia {
|
||||||
return Single.fromCallable {
|
var mediaSize = MEDIA_SIZE_UNKNOWN
|
||||||
var mediaSize = MEDIA_SIZE_UNKNOWN
|
var uri = inUri
|
||||||
var uri = inUri
|
val mimeType: String?
|
||||||
var mimeType: String? = null
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
when (inUri.scheme) {
|
when (inUri.scheme) {
|
||||||
ContentResolver.SCHEME_CONTENT -> {
|
ContentResolver.SCHEME_CONTENT -> {
|
||||||
|
|
||||||
mimeType = contentResolver.getType(uri)
|
mimeType = contentResolver.getType(uri)
|
||||||
|
|
||||||
val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
|
val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
|
||||||
|
|
||||||
contentResolver.openInputStream(inUri).use { input ->
|
contentResolver.openInputStream(inUri).use { input ->
|
||||||
if (input == null) {
|
if (input == null) {
|
||||||
Log.w(TAG, "Media input is null")
|
Log.w(TAG, "Media input is null")
|
||||||
uri = inUri
|
uri = inUri
|
||||||
return@use
|
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 -> {
|
|
||||||
val path = uri.path
|
|
||||||
if (path == null) {
|
|
||||||
Log.w(TAG, "empty uri path $uri")
|
|
||||||
throw CouldNotOpenFileException()
|
|
||||||
}
|
|
||||||
val inputFile = File(path)
|
|
||||||
val suffix = inputFile.name.substringAfterLast('.', "tmp")
|
|
||||||
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix)
|
|
||||||
val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir)
|
|
||||||
val input = FileInputStream(inputFile)
|
|
||||||
|
|
||||||
FileOutputStream(file.absoluteFile).use { out ->
|
FileOutputStream(file.absoluteFile).use { out ->
|
||||||
input.copyTo(out)
|
input.copyTo(out)
|
||||||
uri = FileProvider.getUriForFile(
|
uri = FileProvider.getUriForFile(
|
||||||
|
@ -137,53 +121,74 @@ class MediaUploader @Inject constructor(
|
||||||
mediaSize = getMediaSize(contentResolver, uri)
|
mediaSize = getMediaSize(contentResolver, uri)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
}
|
||||||
Log.w(TAG, "Unknown uri scheme $uri")
|
ContentResolver.SCHEME_FILE -> {
|
||||||
|
val path = uri.path
|
||||||
|
if (path == null) {
|
||||||
|
Log.w(TAG, "empty uri path $uri")
|
||||||
throw CouldNotOpenFileException()
|
throw CouldNotOpenFileException()
|
||||||
}
|
}
|
||||||
}
|
val inputFile = File(path)
|
||||||
} catch (e: IOException) {
|
val suffix = inputFile.name.substringAfterLast('.', "tmp")
|
||||||
Log.w(TAG, e)
|
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix)
|
||||||
throw CouldNotOpenFileException()
|
val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir)
|
||||||
}
|
val input = FileInputStream(inputFile)
|
||||||
if (mediaSize == MEDIA_SIZE_UNKNOWN) {
|
|
||||||
Log.w(TAG, "Could not determine file size of upload")
|
|
||||||
throw MediaTypeException()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mimeType != null) {
|
FileOutputStream(file.absoluteFile).use { out ->
|
||||||
val topLevelType = mimeType.substring(0, mimeType.indexOf('/'))
|
input.copyTo(out)
|
||||||
when (topLevelType) {
|
uri = FileProvider.getUriForFile(
|
||||||
"video" -> {
|
context,
|
||||||
if (mediaSize > STATUS_VIDEO_SIZE_LIMIT) {
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
throw VideoSizeException()
|
file
|
||||||
}
|
)
|
||||||
PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize)
|
mediaSize = getMediaSize(contentResolver, uri)
|
||||||
}
|
|
||||||
"image" -> {
|
|
||||||
PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize)
|
|
||||||
}
|
|
||||||
"audio" -> {
|
|
||||||
if (mediaSize > STATUS_AUDIO_SIZE_LIMIT) {
|
|
||||||
throw AudioSizeException()
|
|
||||||
}
|
|
||||||
PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
throw MediaTypeException()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
else -> {
|
||||||
Log.w(TAG, "Could not determine mime type of upload")
|
Log.w(TAG, "Unknown uri scheme $uri")
|
||||||
throw MediaTypeException()
|
throw CouldNotOpenFileException()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.w(TAG, e)
|
||||||
|
throw CouldNotOpenFileException()
|
||||||
|
}
|
||||||
|
if (mediaSize == MEDIA_SIZE_UNKNOWN) {
|
||||||
|
Log.w(TAG, "Could not determine file size of upload")
|
||||||
|
throw MediaTypeException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mimeType != null) {
|
||||||
|
return when (mimeType.substring(0, mimeType.indexOf('/'))) {
|
||||||
|
"video" -> {
|
||||||
|
if (mediaSize > STATUS_VIDEO_SIZE_LIMIT) {
|
||||||
|
throw VideoSizeException()
|
||||||
|
}
|
||||||
|
PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize)
|
||||||
|
}
|
||||||
|
"image" -> {
|
||||||
|
PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize)
|
||||||
|
}
|
||||||
|
"audio" -> {
|
||||||
|
if (mediaSize > STATUS_AUDIO_SIZE_LIMIT) {
|
||||||
|
throw AudioSizeException()
|
||||||
|
}
|
||||||
|
PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw MediaTypeException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Could not determine mime type of upload")
|
||||||
|
throw MediaTypeException()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val contentResolver = context.contentResolver
|
private val contentResolver = context.contentResolver
|
||||||
|
|
||||||
private fun upload(media: QueuedMedia): Observable<UploadEvent> {
|
private suspend fun upload(media: QueuedMedia): Flow<UploadEvent> {
|
||||||
return Observable.create { emitter ->
|
return callbackFlow {
|
||||||
var mimeType = contentResolver.getType(media.uri)
|
var mimeType = contentResolver.getType(media.uri)
|
||||||
val map = MimeTypeMap.getSingleton()
|
val map = MimeTypeMap.getSingleton()
|
||||||
val fileExtension = map.getExtensionFromMimeType(mimeType)
|
val fileExtension = map.getExtensionFromMimeType(mimeType)
|
||||||
|
@ -200,11 +205,11 @@ class MediaUploader @Inject constructor(
|
||||||
|
|
||||||
var lastProgress = -1
|
var lastProgress = -1
|
||||||
val fileBody = ProgressRequestBody(
|
val fileBody = ProgressRequestBody(
|
||||||
stream, media.mediaSize,
|
stream!!, media.mediaSize,
|
||||||
mimeType.toMediaTypeOrNull()
|
mimeType.toMediaTypeOrNull()!!
|
||||||
) { percentage ->
|
) { percentage ->
|
||||||
if (percentage != lastProgress) {
|
if (percentage != lastProgress) {
|
||||||
emitter.onNext(UploadEvent.ProgressEvent(percentage))
|
trySend(UploadEvent.ProgressEvent(percentage))
|
||||||
}
|
}
|
||||||
lastProgress = percentage
|
lastProgress = percentage
|
||||||
}
|
}
|
||||||
|
@ -217,28 +222,15 @@ class MediaUploader @Inject constructor(
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
val uploadDisposable = mastodonApi.uploadMedia(body, description)
|
val result = mediaUploadApi.uploadMedia(body, description).getOrThrow()
|
||||||
.subscribe(
|
send(UploadEvent.FinishedEvent(result.id))
|
||||||
{ result ->
|
awaitClose()
|
||||||
emitter.onNext(UploadEvent.FinishedEvent(result.id))
|
|
||||||
emitter.onComplete()
|
|
||||||
},
|
|
||||||
{ e ->
|
|
||||||
emitter.onError(e)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Cancel the request when our observable is cancelled
|
|
||||||
emitter.setDisposable(uploadDisposable)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downsize(media: QueuedMedia): QueuedMedia {
|
private fun downsize(media: QueuedMedia): QueuedMedia {
|
||||||
val file = createNewImageFile(context)
|
val file = createNewImageFile(context)
|
||||||
DownsizeImageTask.resize(
|
downsizeImage(media.uri, STATUS_IMAGE_SIZE_LIMIT, contentResolver, file)
|
||||||
arrayOf(media.uri),
|
|
||||||
STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file
|
|
||||||
)
|
|
||||||
return media.copy(uri = file.toUri(), mediaSize = file.length())
|
return media.copy(uri = file.toUri(), mediaSize = file.length())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ import android.widget.LinearLayout
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.lifecycleScope
|
||||||
import at.connyduck.sparkbutton.helpers.Utils
|
import at.connyduck.sparkbutton.helpers.Utils
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy
|
||||||
|
@ -35,7 +35,7 @@ import com.bumptech.glide.request.target.CustomTarget
|
||||||
import com.bumptech.glide.request.transition.Transition
|
import com.bumptech.glide.request.transition.Transition
|
||||||
import com.github.chrisbanes.photoview.PhotoView
|
import com.github.chrisbanes.photoview.PhotoView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.util.withLifecycleContext
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32
|
// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32
|
||||||
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
|
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
|
||||||
|
@ -43,7 +43,7 @@ private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500
|
||||||
fun <T> T.makeCaptionDialog(
|
fun <T> T.makeCaptionDialog(
|
||||||
existingDescription: String?,
|
existingDescription: String?,
|
||||||
previewUri: Uri,
|
previewUri: Uri,
|
||||||
onUpdateDescription: (String) -> LiveData<Boolean>
|
onUpdateDescription: suspend (String) -> Boolean
|
||||||
) where T : Activity, T : LifecycleOwner {
|
) where T : Activity, T : LifecycleOwner {
|
||||||
val dialogLayout = LinearLayout(this)
|
val dialogLayout = LinearLayout(this)
|
||||||
val padding = Utils.dpToPx(this, 8)
|
val padding = Utils.dpToPx(this, 8)
|
||||||
|
@ -77,12 +77,11 @@ fun <T> T.makeCaptionDialog(
|
||||||
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
|
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
|
||||||
|
|
||||||
val okListener = { dialog: DialogInterface, _: Int ->
|
val okListener = { dialog: DialogInterface, _: Int ->
|
||||||
onUpdateDescription(input.text.toString())
|
lifecycleScope.launch {
|
||||||
withLifecycleContext {
|
if (!onUpdateDescription(input.text.toString())) {
|
||||||
onUpdateDescription(input.text.toString())
|
showFailedCaptionMessage()
|
||||||
.observe { success -> if (!success) showFailedCaptionMessage() }
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,7 @@ import androidx.core.view.OnReceiveContentListener
|
||||||
import androidx.core.view.ViewCompat
|
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.emoji2.viewsintegration.EmojiEditTextHelper
|
||||||
|
|
||||||
class EditTextTyped @JvmOverloads constructor(
|
class EditTextTyped @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
|
@ -26,7 +26,7 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||||
class ConversationAdapter(
|
class ConversationAdapter(
|
||||||
private val statusDisplayOptions: StatusDisplayOptions,
|
private val statusDisplayOptions: StatusDisplayOptions,
|
||||||
private val listener: StatusActionListener
|
private val listener: StatusActionListener
|
||||||
) : PagingDataAdapter<ConversationEntity, ConversationViewHolder>(CONVERSATION_COMPARATOR) {
|
) : PagingDataAdapter<ConversationViewData, ConversationViewHolder>(CONVERSATION_COMPARATOR) {
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder {
|
||||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false)
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false)
|
||||||
|
@ -37,17 +37,13 @@ class ConversationAdapter(
|
||||||
holder.setupWithConversation(getItem(position))
|
holder.setupWithConversation(getItem(position))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun item(position: Int): ConversationEntity? {
|
|
||||||
return getItem(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationEntity>() {
|
val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationViewData>() {
|
||||||
override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean {
|
override fun areItemsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean {
|
||||||
return oldItem.id == newItem.id
|
return oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean {
|
override fun areContentsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean {
|
||||||
return oldItem == newItem
|
return oldItem == newItem
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.conversation
|
package com.keylesspalace.tusky.components.conversation
|
||||||
|
|
||||||
import android.text.Spanned
|
|
||||||
import androidx.room.Embedded
|
import androidx.room.Embedded
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
|
@ -27,7 +26,7 @@ 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.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Entity(primaryKeys = ["id", "accountId"])
|
@Entity(primaryKeys = ["id", "accountId"])
|
||||||
|
@ -38,7 +37,16 @@ data class ConversationEntity(
|
||||||
val accounts: List<ConversationAccountEntity>,
|
val accounts: List<ConversationAccountEntity>,
|
||||||
val unread: Boolean,
|
val unread: Boolean,
|
||||||
@Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity
|
@Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity
|
||||||
)
|
) {
|
||||||
|
fun toViewData(): ConversationViewData {
|
||||||
|
return ConversationViewData(
|
||||||
|
id = id,
|
||||||
|
accounts = accounts,
|
||||||
|
unread = unread,
|
||||||
|
lastStatus = lastStatus.toViewData()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class ConversationAccountEntity(
|
data class ConversationAccountEntity(
|
||||||
val id: String,
|
val id: String,
|
||||||
|
@ -67,7 +75,7 @@ data class ConversationStatusEntity(
|
||||||
val inReplyToId: String?,
|
val inReplyToId: String?,
|
||||||
val inReplyToAccountId: String?,
|
val inReplyToAccountId: String?,
|
||||||
val account: ConversationAccountEntity,
|
val account: ConversationAccountEntity,
|
||||||
val content: Spanned,
|
val content: String,
|
||||||
val createdAt: Date,
|
val createdAt: Date,
|
||||||
val emojis: List<Emoji>,
|
val emojis: List<Emoji>,
|
||||||
val favouritesCount: Int,
|
val favouritesCount: Int,
|
||||||
|
@ -80,95 +88,43 @@ data class ConversationStatusEntity(
|
||||||
val tags: List<HashTag>?,
|
val tags: List<HashTag>?,
|
||||||
val showingHiddenContent: Boolean,
|
val showingHiddenContent: Boolean,
|
||||||
val expanded: Boolean,
|
val expanded: Boolean,
|
||||||
val collapsible: Boolean,
|
|
||||||
val collapsed: Boolean,
|
val collapsed: Boolean,
|
||||||
val muted: Boolean,
|
val muted: Boolean,
|
||||||
val poll: Poll?
|
val poll: Poll?
|
||||||
) {
|
) {
|
||||||
/** its necessary to override this because Spanned.equals does not work as expected */
|
|
||||||
override fun equals(other: Any?): Boolean {
|
|
||||||
if (this === other) return true
|
|
||||||
if (javaClass != other?.javaClass) return false
|
|
||||||
|
|
||||||
other as ConversationStatusEntity
|
fun toViewData(): StatusViewData.Concrete {
|
||||||
|
return StatusViewData.Concrete(
|
||||||
if (id != other.id) return false
|
status = Status(
|
||||||
if (url != other.url) return false
|
id = id,
|
||||||
if (inReplyToId != other.inReplyToId) return false
|
url = url,
|
||||||
if (inReplyToAccountId != other.inReplyToAccountId) return false
|
account = account.toAccount(),
|
||||||
if (account != other.account) return false
|
inReplyToId = inReplyToId,
|
||||||
if (content.toString() != other.content.toString()) return false
|
inReplyToAccountId = inReplyToAccountId,
|
||||||
if (createdAt != other.createdAt) return false
|
content = content,
|
||||||
if (emojis != other.emojis) return false
|
reblog = null,
|
||||||
if (favouritesCount != other.favouritesCount) return false
|
createdAt = createdAt,
|
||||||
if (favourited != other.favourited) return false
|
emojis = emojis,
|
||||||
if (sensitive != other.sensitive) return false
|
reblogsCount = 0,
|
||||||
if (spoilerText != other.spoilerText) return false
|
favouritesCount = favouritesCount,
|
||||||
if (attachments != other.attachments) return false
|
reblogged = false,
|
||||||
if (mentions != other.mentions) return false
|
favourited = favourited,
|
||||||
if (tags != other.tags) return false
|
bookmarked = bookmarked,
|
||||||
if (showingHiddenContent != other.showingHiddenContent) return false
|
sensitive = sensitive,
|
||||||
if (expanded != other.expanded) return false
|
spoilerText = spoilerText,
|
||||||
if (collapsible != other.collapsible) return false
|
visibility = Status.Visibility.DIRECT,
|
||||||
if (collapsed != other.collapsed) return false
|
attachments = attachments,
|
||||||
if (muted != other.muted) return false
|
mentions = mentions,
|
||||||
if (poll != other.poll) return false
|
tags = tags,
|
||||||
|
application = null,
|
||||||
return true
|
pinned = false,
|
||||||
}
|
muted = muted,
|
||||||
|
poll = poll,
|
||||||
override fun hashCode(): Int {
|
card = null
|
||||||
var result = id.hashCode()
|
),
|
||||||
result = 31 * result + (url?.hashCode() ?: 0)
|
isExpanded = expanded,
|
||||||
result = 31 * result + (inReplyToId?.hashCode() ?: 0)
|
isShowingContent = showingHiddenContent,
|
||||||
result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0)
|
isCollapsed = collapsed
|
||||||
result = 31 * result + account.hashCode()
|
|
||||||
result = 31 * result + content.toString().hashCode()
|
|
||||||
result = 31 * result + createdAt.hashCode()
|
|
||||||
result = 31 * result + emojis.hashCode()
|
|
||||||
result = 31 * result + favouritesCount
|
|
||||||
result = 31 * result + favourited.hashCode()
|
|
||||||
result = 31 * result + sensitive.hashCode()
|
|
||||||
result = 31 * result + spoilerText.hashCode()
|
|
||||||
result = 31 * result + attachments.hashCode()
|
|
||||||
result = 31 * result + mentions.hashCode()
|
|
||||||
result = 31 * result + tags.hashCode()
|
|
||||||
result = 31 * result + showingHiddenContent.hashCode()
|
|
||||||
result = 31 * result + expanded.hashCode()
|
|
||||||
result = 31 * result + collapsible.hashCode()
|
|
||||||
result = 31 * result + collapsed.hashCode()
|
|
||||||
result = 31 * result + muted.hashCode()
|
|
||||||
result = 31 * result + poll.hashCode()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun toStatus(): Status {
|
|
||||||
return Status(
|
|
||||||
id = id,
|
|
||||||
url = url,
|
|
||||||
account = account.toAccount(),
|
|
||||||
inReplyToId = inReplyToId,
|
|
||||||
inReplyToAccountId = inReplyToAccountId,
|
|
||||||
content = content,
|
|
||||||
reblog = null,
|
|
||||||
createdAt = createdAt,
|
|
||||||
emojis = emojis,
|
|
||||||
reblogsCount = 0,
|
|
||||||
favouritesCount = favouritesCount,
|
|
||||||
reblogged = false,
|
|
||||||
favourited = favourited,
|
|
||||||
bookmarked = bookmarked,
|
|
||||||
sensitive = sensitive,
|
|
||||||
spoilerText = spoilerText,
|
|
||||||
visibility = Status.Visibility.DIRECT,
|
|
||||||
attachments = attachments,
|
|
||||||
mentions = mentions,
|
|
||||||
tags = tags,
|
|
||||||
application = null,
|
|
||||||
pinned = false,
|
|
||||||
muted = muted,
|
|
||||||
poll = poll,
|
|
||||||
card = null
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -202,7 +158,6 @@ fun Status.toEntity() =
|
||||||
tags = tags,
|
tags = tags,
|
||||||
showingHiddenContent = false,
|
showingHiddenContent = false,
|
||||||
expanded = false,
|
expanded = false,
|
||||||
collapsible = shouldTrimStatus(content),
|
|
||||||
collapsed = true,
|
collapsed = true,
|
||||||
muted = muted ?: false,
|
muted = muted ?: false,
|
||||||
poll = poll
|
poll = poll
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
/* Copyright 2022 Tusky Contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.components.conversation
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
|
||||||
|
data class ConversationViewData(
|
||||||
|
val id: String,
|
||||||
|
val accounts: List<ConversationAccountEntity>,
|
||||||
|
val unread: Boolean,
|
||||||
|
val lastStatus: StatusViewData.Concrete
|
||||||
|
) {
|
||||||
|
fun toEntity(
|
||||||
|
accountId: Long,
|
||||||
|
favourited: Boolean = lastStatus.status.favourited,
|
||||||
|
bookmarked: Boolean = lastStatus.status.bookmarked,
|
||||||
|
muted: Boolean = lastStatus.status.muted ?: false,
|
||||||
|
poll: Poll? = lastStatus.status.poll,
|
||||||
|
expanded: Boolean = lastStatus.isExpanded,
|
||||||
|
collapsed: Boolean = lastStatus.isCollapsed,
|
||||||
|
showingHiddenContent: Boolean = lastStatus.isShowingContent
|
||||||
|
): ConversationEntity {
|
||||||
|
return ConversationEntity(
|
||||||
|
accountId = accountId,
|
||||||
|
id = id,
|
||||||
|
accounts = accounts,
|
||||||
|
unread = unread,
|
||||||
|
lastStatus = lastStatus.toConversationStatusEntity(
|
||||||
|
favourited = favourited,
|
||||||
|
bookmarked = bookmarked,
|
||||||
|
muted = muted,
|
||||||
|
poll = poll,
|
||||||
|
expanded = expanded,
|
||||||
|
collapsed = collapsed,
|
||||||
|
showingHiddenContent = showingHiddenContent
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun StatusViewData.Concrete.toConversationStatusEntity(
|
||||||
|
favourited: Boolean = status.favourited,
|
||||||
|
bookmarked: Boolean = status.bookmarked,
|
||||||
|
muted: Boolean = status.muted ?: false,
|
||||||
|
poll: Poll? = status.poll,
|
||||||
|
expanded: Boolean = isExpanded,
|
||||||
|
collapsed: Boolean = isCollapsed,
|
||||||
|
showingHiddenContent: Boolean = isShowingContent
|
||||||
|
): ConversationStatusEntity {
|
||||||
|
return ConversationStatusEntity(
|
||||||
|
id = id,
|
||||||
|
url = status.url,
|
||||||
|
inReplyToId = status.inReplyToId,
|
||||||
|
inReplyToAccountId = status.inReplyToAccountId,
|
||||||
|
account = status.account.toEntity(),
|
||||||
|
content = status.content,
|
||||||
|
createdAt = status.createdAt,
|
||||||
|
emojis = status.emojis,
|
||||||
|
favouritesCount = status.favouritesCount,
|
||||||
|
favourited = favourited,
|
||||||
|
bookmarked = bookmarked,
|
||||||
|
sensitive = status.sensitive,
|
||||||
|
spoilerText = status.spoilerText,
|
||||||
|
attachments = status.attachments,
|
||||||
|
mentions = status.mentions,
|
||||||
|
tags = status.tags,
|
||||||
|
showingHiddenContent = showingHiddenContent,
|
||||||
|
expanded = expanded,
|
||||||
|
collapsed = collapsed,
|
||||||
|
muted = muted,
|
||||||
|
poll = poll
|
||||||
|
)
|
||||||
|
}
|
|
@ -28,11 +28,14 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder;
|
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder;
|
||||||
import com.keylesspalace.tusky.entity.Attachment;
|
import com.keylesspalace.tusky.entity.Attachment;
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
import com.keylesspalace.tusky.entity.TimelineAccount;
|
||||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||||
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
import com.keylesspalace.tusky.util.SmartLengthInputFilter;
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||||
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
|
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@ -69,11 +72,12 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height);
|
return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupWithConversation(ConversationEntity conversation) {
|
void setupWithConversation(ConversationViewData conversation) {
|
||||||
ConversationStatusEntity status = conversation.getLastStatus();
|
StatusViewData.Concrete statusViewData = conversation.getLastStatus();
|
||||||
ConversationAccountEntity account = status.getAccount();
|
Status status = statusViewData.getStatus();
|
||||||
|
TimelineAccount account = status.getAccount();
|
||||||
|
|
||||||
setupCollapsedState(status.getCollapsible(), status.getCollapsed(), status.getExpanded(), status.getSpoilerText(), listener);
|
setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), statusViewData.getSpoilerText(), listener);
|
||||||
|
|
||||||
setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions);
|
setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions);
|
||||||
setUsername(account.getUsername());
|
setUsername(account.getUsername());
|
||||||
|
@ -84,7 +88,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
List<Attachment> attachments = status.getAttachments();
|
List<Attachment> attachments = status.getAttachments();
|
||||||
boolean sensitive = status.getSensitive();
|
boolean sensitive = status.getSensitive();
|
||||||
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
|
if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) {
|
||||||
setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(),
|
setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(),
|
||||||
statusDisplayOptions.useBlurhash());
|
statusDisplayOptions.useBlurhash());
|
||||||
|
|
||||||
if (attachments.size() == 0) {
|
if (attachments.size() == 0) {
|
||||||
|
@ -95,7 +99,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
mediaLabel.setVisibility(View.GONE);
|
mediaLabel.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setMediaLabel(attachments, sensitive, listener, status.getShowingHiddenContent());
|
setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent());
|
||||||
// Hide all unused views.
|
// Hide all unused views.
|
||||||
mediaPreviews[0].setVisibility(View.GONE);
|
mediaPreviews[0].setVisibility(View.GONE);
|
||||||
mediaPreviews[1].setVisibility(View.GONE);
|
mediaPreviews[1].setVisibility(View.GONE);
|
||||||
|
@ -104,10 +108,10 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
hideSensitiveMediaWarning();
|
hideSensitiveMediaWarning();
|
||||||
}
|
}
|
||||||
|
|
||||||
setupButtons(listener, account.getId(), status.getContent().toString(),
|
setupButtons(listener, account.getId(), statusViewData.getContent().toString(),
|
||||||
statusDisplayOptions);
|
statusDisplayOptions);
|
||||||
|
|
||||||
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(),
|
setSpoilerAndContent(statusViewData.isExpanded(), statusViewData.getContent(), status.getSpoilerText(),
|
||||||
status.getMentions(), status.getTags(), status.getEmojis(),
|
status.getMentions(), status.getTags(), status.getEmojis(),
|
||||||
PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener);
|
PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener);
|
||||||
|
|
||||||
|
|
|
@ -153,24 +153,24 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFavourite(favourite: Boolean, position: Int) {
|
override fun onFavourite(favourite: Boolean, position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewModel.favourite(favourite, conversation)
|
viewModel.favourite(favourite, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBookmark(favourite: Boolean, position: Int) {
|
override fun onBookmark(favourite: Boolean, position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewModel.bookmark(favourite, conversation)
|
viewModel.bookmark(favourite, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMore(view: View, position: Int) {
|
override fun onMore(view: View, position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
|
|
||||||
val popup = PopupMenu(requireContext(), view)
|
val popup = PopupMenu(requireContext(), view)
|
||||||
popup.inflate(R.menu.conversation_more)
|
popup.inflate(R.menu.conversation_more)
|
||||||
|
|
||||||
if (conversation.lastStatus.muted) {
|
if (conversation.lastStatus.status.muted == true) {
|
||||||
popup.menu.removeItem(R.id.status_mute_conversation)
|
popup.menu.removeItem(R.id.status_mute_conversation)
|
||||||
} else {
|
} else {
|
||||||
popup.menu.removeItem(R.id.status_unmute_conversation)
|
popup.menu.removeItem(R.id.status_unmute_conversation)
|
||||||
|
@ -189,14 +189,14 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.toStatus()), view)
|
viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.status), view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewThread(position: Int) {
|
override fun onViewThread(position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewThread(conversation.lastStatus.id, conversation.lastStatus.url)
|
viewThread(conversation.lastStatus.id, conversation.lastStatus.status.url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,13 +205,13 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
override fun onExpandedChange(expanded: Boolean, position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewModel.expandHiddenStatus(expanded, conversation)
|
viewModel.expandHiddenStatus(expanded, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
override fun onContentHiddenChange(isShowing: Boolean, position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewModel.showContent(isShowing, conversation)
|
viewModel.showContent(isShowing, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -221,7 +221,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewModel.collapseLongStatus(isCollapsed, conversation)
|
viewModel.collapseLongStatus(isCollapsed, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -241,12 +241,12 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReply(position: Int) {
|
override fun onReply(position: Int) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
reply(conversation.lastStatus.toStatus())
|
reply(conversation.lastStatus.status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteConversation(conversation: ConversationEntity) {
|
private fun deleteConversation(conversation: ConversationViewData) {
|
||||||
AlertDialog.Builder(requireContext())
|
AlertDialog.Builder(requireContext())
|
||||||
.setMessage(R.string.dialog_delete_conversation_warning)
|
.setMessage(R.string.dialog_delete_conversation_warning)
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
@ -268,7 +268,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
override fun onVoteInPoll(position: Int, choices: MutableList<Int>) {
|
||||||
adapter.item(position)?.let { conversation ->
|
adapter.peek(position)?.let { conversation ->
|
||||||
viewModel.voteInPoll(choices, conversation)
|
viewModel.voteInPoll(choices, conversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,16 +16,18 @@
|
||||||
package com.keylesspalace.tusky.components.conversation
|
package com.keylesspalace.tusky.components.conversation
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.ExperimentalPagingApi
|
import androidx.paging.ExperimentalPagingApi
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
|
import androidx.paging.map
|
||||||
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.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.RxAwareViewModel
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
@ -35,7 +37,7 @@ class ConversationsViewModel @Inject constructor(
|
||||||
private val database: AppDatabase,
|
private val database: AppDatabase,
|
||||||
private val accountManager: AccountManager,
|
private val accountManager: AccountManager,
|
||||||
private val api: MastodonApi
|
private val api: MastodonApi
|
||||||
) : RxAwareViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
@OptIn(ExperimentalPagingApi::class)
|
@OptIn(ExperimentalPagingApi::class)
|
||||||
val conversationFlow = Pager(
|
val conversationFlow = Pager(
|
||||||
|
@ -44,104 +46,117 @@ class ConversationsViewModel @Inject constructor(
|
||||||
pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) }
|
pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) }
|
||||||
)
|
)
|
||||||
.flow
|
.flow
|
||||||
|
.map { pagingData ->
|
||||||
|
pagingData.map { conversation -> conversation.toViewData() }
|
||||||
|
}
|
||||||
.cachedIn(viewModelScope)
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
fun favourite(favourite: Boolean, conversation: ConversationEntity) {
|
fun favourite(favourite: Boolean, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
timelineCases.favourite(conversation.lastStatus.id, favourite).await()
|
timelineCases.favourite(conversation.lastStatus.id, favourite).await()
|
||||||
|
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.toEntity(
|
||||||
lastStatus = conversation.lastStatus.copy(favourited = favourite)
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
favourited = favourite
|
||||||
)
|
)
|
||||||
|
|
||||||
database.conversationDao().insert(newConversation)
|
saveConversationToDb(newConversation)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "failed to favourite status", e)
|
Log.w(TAG, "failed to favourite status", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bookmark(bookmark: Boolean, conversation: ConversationEntity) {
|
fun bookmark(bookmark: Boolean, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
timelineCases.bookmark(conversation.lastStatus.id, bookmark).await()
|
timelineCases.bookmark(conversation.lastStatus.id, bookmark).await()
|
||||||
|
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.toEntity(
|
||||||
lastStatus = conversation.lastStatus.copy(bookmarked = bookmark)
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
bookmarked = bookmark
|
||||||
)
|
)
|
||||||
|
|
||||||
database.conversationDao().insert(newConversation)
|
saveConversationToDb(newConversation)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "failed to bookmark status", e)
|
Log.w(TAG, "failed to bookmark status", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun voteInPoll(choices: List<Int>, conversation: ConversationEntity) {
|
fun voteInPoll(choices: List<Int>, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.poll?.id!!, choices).await()
|
val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.status.poll?.id!!, choices).await()
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.toEntity(
|
||||||
lastStatus = conversation.lastStatus.copy(poll = poll)
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
poll = poll
|
||||||
)
|
)
|
||||||
|
|
||||||
database.conversationDao().insert(newConversation)
|
saveConversationToDb(newConversation)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "failed to vote in poll", e)
|
Log.w(TAG, "failed to vote in poll", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun expandHiddenStatus(expanded: Boolean, conversation: ConversationEntity) {
|
fun expandHiddenStatus(expanded: Boolean, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.toEntity(
|
||||||
lastStatus = conversation.lastStatus.copy(expanded = expanded)
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
expanded = expanded
|
||||||
)
|
)
|
||||||
saveConversationToDb(newConversation)
|
saveConversationToDb(newConversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun collapseLongStatus(collapsed: Boolean, conversation: ConversationEntity) {
|
fun collapseLongStatus(collapsed: Boolean, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.toEntity(
|
||||||
lastStatus = conversation.lastStatus.copy(collapsed = collapsed)
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
collapsed = collapsed
|
||||||
)
|
)
|
||||||
saveConversationToDb(newConversation)
|
saveConversationToDb(newConversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showContent(showing: Boolean, conversation: ConversationEntity) {
|
fun showContent(showing: Boolean, conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.toEntity(
|
||||||
lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing)
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
showingHiddenContent = showing
|
||||||
)
|
)
|
||||||
saveConversationToDb(newConversation)
|
saveConversationToDb(newConversation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun remove(conversation: ConversationEntity) {
|
fun remove(conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
api.deleteConversation(conversationId = conversation.id)
|
api.deleteConversation(conversationId = conversation.id)
|
||||||
|
|
||||||
database.conversationDao().delete(conversation)
|
database.conversationDao().delete(
|
||||||
|
id = conversation.id,
|
||||||
|
accountId = accountManager.activeAccount!!.id
|
||||||
|
)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.w(TAG, "failed to delete conversation", e)
|
Log.w(TAG, "failed to delete conversation", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun muteConversation(conversation: ConversationEntity) {
|
fun muteConversation(conversation: ConversationViewData) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
try {
|
||||||
val newStatus = timelineCases.muteConversation(
|
timelineCases.muteConversation(
|
||||||
conversation.lastStatus.id,
|
conversation.lastStatus.id,
|
||||||
!conversation.lastStatus.muted
|
!(conversation.lastStatus.status.muted ?: false)
|
||||||
).await()
|
).await()
|
||||||
|
|
||||||
val newConversation = conversation.copy(
|
val newConversation = conversation.toEntity(
|
||||||
lastStatus = newStatus.toEntity()
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
muted = !(conversation.lastStatus.status.muted ?: false)
|
||||||
)
|
)
|
||||||
|
|
||||||
database.conversationDao().insert(newConversation)
|
database.conversationDao().insert(newConversation)
|
||||||
|
@ -151,7 +166,7 @@ class ConversationsViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun saveConversationToDb(conversation: ConversationEntity) {
|
private suspend fun saveConversationToDb(conversation: ConversationEntity) {
|
||||||
database.conversationDao().insert(conversation)
|
database.conversationDao().insert(conversation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,12 @@ import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.util.IOUtils
|
import com.keylesspalace.tusky.util.IOUtils
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
@ -38,6 +43,7 @@ import javax.inject.Inject
|
||||||
|
|
||||||
class DraftHelper @Inject constructor(
|
class DraftHelper @Inject constructor(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
|
val okHttpClient: OkHttpClient,
|
||||||
db: AppDatabase
|
db: AppDatabase
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -71,11 +77,11 @@ class DraftHelper @Inject constructor(
|
||||||
|
|
||||||
val uris = mediaUris.map { uriString ->
|
val uris = mediaUris.map { uriString ->
|
||||||
uriString.toUri()
|
uriString.toUri()
|
||||||
}.map { uri ->
|
}.mapNotNull { uri ->
|
||||||
if (uri.isNotInFolder(draftDirectory)) {
|
if (uri.isInFolder(draftDirectory)) {
|
||||||
uri.copyToFolder(draftDirectory)
|
|
||||||
} else {
|
|
||||||
uri
|
uri
|
||||||
|
} else {
|
||||||
|
uri.copyToFolder(draftDirectory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,6 +120,7 @@ class DraftHelper @Inject constructor(
|
||||||
)
|
)
|
||||||
|
|
||||||
draftDao.insertOrReplace(draft)
|
draftDao.insertOrReplace(draft)
|
||||||
|
Log.d("DraftHelper", "saved draft to db")
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteDraftAndAttachments(draftId: Int) {
|
suspend fun deleteDraftAndAttachments(draftId: Int) {
|
||||||
|
@ -133,33 +140,55 @@ class DraftHelper @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun deleteAttachments(draft: DraftEntity) {
|
suspend fun deleteAttachments(draft: DraftEntity) = withContext(Dispatchers.IO) {
|
||||||
withContext(Dispatchers.IO) {
|
draft.attachments.forEach { attachment ->
|
||||||
draft.attachments.forEach { attachment ->
|
if (context.contentResolver.delete(attachment.uri, null, null) == 0) {
|
||||||
if (context.contentResolver.delete(attachment.uri, null, null) == 0) {
|
Log.e("DraftHelper", "Did not delete file ${attachment.uriString}")
|
||||||
Log.e("DraftHelper", "Did not delete file ${attachment.uriString}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Uri.isNotInFolder(folder: File): Boolean {
|
private fun Uri.isInFolder(folder: File): Boolean {
|
||||||
val filePath = path ?: return true
|
val filePath = path ?: return true
|
||||||
return File(filePath).parentFile == folder
|
return File(filePath).parentFile == folder
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Uri.copyToFolder(folder: File): Uri {
|
private fun Uri.copyToFolder(folder: File): Uri? {
|
||||||
val contentResolver = context.contentResolver
|
val contentResolver = context.contentResolver
|
||||||
|
|
||||||
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||||
|
|
||||||
val mimeType = contentResolver.getType(this)
|
val fileExtension = if (scheme == "https") {
|
||||||
val map = MimeTypeMap.getSingleton()
|
lastPathSegment?.substringAfterLast('.', "tmp")
|
||||||
val fileExtension = map.getExtensionFromMimeType(mimeType)
|
} else {
|
||||||
|
val mimeType = contentResolver.getType(this)
|
||||||
|
val map = MimeTypeMap.getSingleton()
|
||||||
|
map.getExtensionFromMimeType(mimeType)
|
||||||
|
}
|
||||||
|
|
||||||
val filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension)
|
val filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension)
|
||||||
val file = File(folder, filename)
|
val file = File(folder, filename)
|
||||||
IOUtils.copyToFile(contentResolver, this, file)
|
|
||||||
|
if (scheme == "https") {
|
||||||
|
// saving redrafted media
|
||||||
|
try {
|
||||||
|
val request = Request.Builder().url(toString()).build()
|
||||||
|
|
||||||
|
val response = okHttpClient.newCall(request).execute()
|
||||||
|
|
||||||
|
val sink = file.sink().buffer()
|
||||||
|
|
||||||
|
response.body?.source()?.use { input ->
|
||||||
|
sink.use { output ->
|
||||||
|
output.writeAll(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: IOException) {
|
||||||
|
Log.w("DraftHelper", "failed to save media", ex)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
IOUtils.copyToFile(contentResolver, this, file)
|
||||||
|
}
|
||||||
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
|
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,7 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.databinding.ActivityDraftsBinding
|
import com.keylesspalace.tusky.databinding.ActivityDraftsBinding
|
||||||
import com.keylesspalace.tusky.db.DraftEntity
|
import com.keylesspalace.tusky.db.DraftEntity
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
|
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||||
import com.keylesspalace.tusky.util.visible
|
import com.keylesspalace.tusky.util.visible
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
@ -100,7 +101,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
content = draft.content,
|
content = draft.content,
|
||||||
contentWarning = draft.contentWarning,
|
contentWarning = draft.contentWarning,
|
||||||
inReplyToId = draft.inReplyToId,
|
inReplyToId = draft.inReplyToId,
|
||||||
replyingStatusContent = status.content.toString(),
|
replyingStatusContent = status.content.parseAsMastodonHtml().toString(),
|
||||||
replyingStatusAuthor = status.account.localUsername,
|
replyingStatusAuthor = status.account.localUsername,
|
||||||
draftAttachments = draft.attachments,
|
draftAttachments = draft.attachments,
|
||||||
poll = draft.poll,
|
poll = draft.poll,
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
/* Copyright 2022 Tusky contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.components.instanceinfo
|
||||||
|
|
||||||
|
data class InstanceInfo(
|
||||||
|
val maxChars: Int,
|
||||||
|
val pollMaxOptions: Int,
|
||||||
|
val pollMaxLength: Int,
|
||||||
|
val pollMinDuration: Int,
|
||||||
|
val pollMaxDuration: Int,
|
||||||
|
val charactersReservedPerUrl: Int
|
||||||
|
)
|
|
@ -0,0 +1,102 @@
|
||||||
|
/* Copyright 2022 Tusky contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.components.instanceinfo
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
|
import com.keylesspalace.tusky.db.EmojisEntity
|
||||||
|
import com.keylesspalace.tusky.db.InstanceInfoEntity
|
||||||
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class InstanceInfoRepository @Inject constructor(
|
||||||
|
private val api: MastodonApi,
|
||||||
|
db: AppDatabase,
|
||||||
|
accountManager: AccountManager
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val dao = db.instanceDao()
|
||||||
|
private val instanceName = accountManager.activeAccount!!.domain
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the custom emojis of the instance.
|
||||||
|
* Will always try to fetch them from the api, falls back to cached Emojis in case it is not available.
|
||||||
|
* Never throws, returns empty list in case of error.
|
||||||
|
*/
|
||||||
|
suspend fun getEmojis(): List<Emoji> = withContext(Dispatchers.IO) {
|
||||||
|
api.getCustomEmojis()
|
||||||
|
.onSuccess { emojiList -> dao.insertOrReplace(EmojisEntity(instanceName, emojiList)) }
|
||||||
|
.getOrElse { throwable ->
|
||||||
|
Log.w(TAG, "failed to load custom emojis, falling back to cache", throwable)
|
||||||
|
dao.getEmojiInfo(instanceName)?.emojiList.orEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns information about the instance.
|
||||||
|
* Will always try to fetch the most up-to-date data from the api, falls back to cache in case it is not available.
|
||||||
|
* Never throws, returns defaults of vanilla Mastodon in case of error.
|
||||||
|
*/
|
||||||
|
suspend fun getInstanceInfo(): InstanceInfo = withContext(Dispatchers.IO) {
|
||||||
|
api.getInstance()
|
||||||
|
.fold(
|
||||||
|
{ instance ->
|
||||||
|
val instanceEntity = InstanceInfoEntity(
|
||||||
|
instance = instanceName,
|
||||||
|
maximumTootCharacters = instance.configuration?.statuses?.maxCharacters ?: instance.maxTootChars,
|
||||||
|
maxPollOptions = instance.configuration?.polls?.maxOptions ?: instance.pollConfiguration?.maxOptions,
|
||||||
|
maxPollOptionLength = instance.configuration?.polls?.maxCharactersPerOption ?: instance.pollConfiguration?.maxOptionChars,
|
||||||
|
minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration,
|
||||||
|
maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration,
|
||||||
|
charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl,
|
||||||
|
version = instance.version
|
||||||
|
)
|
||||||
|
dao.insertOrReplace(instanceEntity)
|
||||||
|
instanceEntity
|
||||||
|
},
|
||||||
|
{ throwable ->
|
||||||
|
Log.w(TAG, "failed to instance, falling back to cache and default values", throwable)
|
||||||
|
dao.getInstanceInfo(instanceName)
|
||||||
|
}
|
||||||
|
).let { instanceInfo: InstanceInfoEntity? ->
|
||||||
|
InstanceInfo(
|
||||||
|
maxChars = instanceInfo?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT,
|
||||||
|
pollMaxOptions = instanceInfo?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT,
|
||||||
|
pollMaxLength = instanceInfo?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
|
||||||
|
pollMinDuration = instanceInfo?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION,
|
||||||
|
pollMaxDuration = instanceInfo?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION,
|
||||||
|
charactersReservedPerUrl = instanceInfo?.charactersReservedPerUrl ?: DEFAULT_CHARACTERS_RESERVED_PER_URL
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "InstanceInfoRepo"
|
||||||
|
|
||||||
|
const val DEFAULT_CHARACTER_LIMIT = 500
|
||||||
|
private const val DEFAULT_MAX_OPTION_COUNT = 4
|
||||||
|
private const val DEFAULT_MAX_OPTION_LENGTH = 50
|
||||||
|
private const val DEFAULT_MIN_POLL_DURATION = 300
|
||||||
|
private const val DEFAULT_MAX_POLL_DURATION = 604800
|
||||||
|
|
||||||
|
// Mastodon only counts URLs as this long in terms of status character limits
|
||||||
|
const val DEFAULT_CHARACTERS_RESERVED_PER_URL = 23
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,7 +34,6 @@ import com.keylesspalace.tusky.MainActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.databinding.ActivityLoginBinding
|
import com.keylesspalace.tusky.databinding.ActivityLoginBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.entity.AppCredentials
|
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.getNonNullString
|
import com.keylesspalace.tusky.util.getNonNullString
|
||||||
import com.keylesspalace.tusky.util.rickRoll
|
import com.keylesspalace.tusky.util.rickRoll
|
||||||
|
@ -70,7 +69,9 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
// Authorization failed. Put the error response where the user can read it and they
|
// Authorization failed. Put the error response where the user can read it and they
|
||||||
// can try again.
|
// can try again.
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied)
|
// Use error returned by the server or fall back to the generic message
|
||||||
|
binding.domainTextInputLayout.error =
|
||||||
|
result.errorMessage.ifBlank { getString(R.string.error_authorization_denied) }
|
||||||
Log.e(
|
Log.e(
|
||||||
TAG,
|
TAG,
|
||||||
"%s %s".format(
|
"%s %s".format(
|
||||||
|
@ -180,32 +181,33 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val credentials: AppCredentials = try {
|
mastodonApi.authenticateApp(
|
||||||
mastodonApi.authenticateApp(
|
domain, getString(R.string.app_name), oauthRedirectUri,
|
||||||
domain, getString(R.string.app_name), oauthRedirectUri,
|
OAUTH_SCOPES, getString(R.string.tusky_website)
|
||||||
OAUTH_SCOPES, getString(R.string.tusky_website)
|
).fold(
|
||||||
)
|
{ credentials ->
|
||||||
} catch (e: Exception) {
|
// Before we open browser page we save the data.
|
||||||
binding.loginButton.isEnabled = true
|
// Even if we don't open other apps user may go to password manager or somewhere else
|
||||||
binding.domainTextInputLayout.error =
|
// and we will need to pick up the process where we left off.
|
||||||
getString(R.string.error_failed_app_registration)
|
// Alternatively we could pass it all as part of the intent and receive it back
|
||||||
setLoading(false)
|
// but it is a bit of a workaround.
|
||||||
Log.e(TAG, Log.getStackTraceString(e))
|
preferences.edit()
|
||||||
return@launch
|
.putString(DOMAIN, domain)
|
||||||
}
|
.putString(CLIENT_ID, credentials.clientId)
|
||||||
|
.putString(CLIENT_SECRET, credentials.clientSecret)
|
||||||
|
.apply()
|
||||||
|
|
||||||
// Before we open browser page we save the data.
|
redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
|
||||||
// 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.
|
{ e ->
|
||||||
// Alternatively we could pass it all as part of the intent and receive it back
|
binding.loginButton.isEnabled = true
|
||||||
// but it is a bit of a workaround.
|
binding.domainTextInputLayout.error =
|
||||||
preferences.edit()
|
getString(R.string.error_failed_app_registration)
|
||||||
.putString(DOMAIN, domain)
|
setLoading(false)
|
||||||
.putString(CLIENT_ID, credentials.clientId)
|
Log.e(TAG, Log.getStackTraceString(e))
|
||||||
.putString(CLIENT_SECRET, credentials.clientSecret)
|
return@launch
|
||||||
.apply()
|
}
|
||||||
|
)
|
||||||
redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,29 +240,28 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
||||||
val accessToken = try {
|
mastodonApi.fetchOAuthToken(
|
||||||
mastodonApi.fetchOAuthToken(
|
domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code"
|
||||||
domain, clientId, clientSecret, oauthRedirectUri, code,
|
).fold(
|
||||||
"authorization_code"
|
{ accessToken ->
|
||||||
)
|
accountManager.addAccount(accessToken.accessToken, domain)
|
||||||
} 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
|
||||||
val intent = Intent(this, MainActivity::class.java)
|
startActivity(intent)
|
||||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
finish()
|
||||||
startActivity(intent)
|
overridePendingTransition(R.anim.explode, R.anim.explode)
|
||||||
finish()
|
},
|
||||||
overridePendingTransition(R.anim.explode, R.anim.explode)
|
{ e ->
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setLoading(loadingState: Boolean) {
|
private fun setLoading(loadingState: Boolean) {
|
||||||
|
|
|
@ -16,10 +16,13 @@ import android.webkit.WebStorage
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.webkit.WebViewClient
|
import android.webkit.WebViewClient
|
||||||
import androidx.activity.result.contract.ActivityResultContract
|
import androidx.activity.result.contract.ActivityResultContract
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.keylesspalace.tusky.BaseActivity
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.databinding.LoginWebviewBinding
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.databinding.ActivityLoginWebviewBinding
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
|
@ -75,7 +78,7 @@ sealed class LoginResult : Parcelable {
|
||||||
|
|
||||||
/** Activity to do Oauth process using WebView. */
|
/** Activity to do Oauth process using WebView. */
|
||||||
class LoginWebViewActivity : BaseActivity(), Injectable {
|
class LoginWebViewActivity : BaseActivity(), Injectable {
|
||||||
private val binding by viewBinding(LoginWebviewBinding::inflate)
|
private val binding by viewBinding(ActivityLoginWebviewBinding::inflate)
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -86,7 +89,9 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
|
||||||
|
|
||||||
setSupportActionBar(binding.loginToolbar)
|
setSupportActionBar(binding.loginToolbar)
|
||||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
supportActionBar?.setDisplayShowTitleEnabled(true)
|
||||||
|
|
||||||
|
setTitle(R.string.title_login)
|
||||||
|
|
||||||
val webView = binding.loginWebView
|
val webView = binding.loginWebView
|
||||||
webView.settings.allowContentAccess = false
|
webView.settings.allowContentAccess = false
|
||||||
|
@ -102,20 +107,34 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
|
||||||
val oauthUrl = data.oauthRedirectUrl
|
val oauthUrl = data.oauthRedirectUrl
|
||||||
|
|
||||||
webView.webViewClient = object : WebViewClient() {
|
webView.webViewClient = object : WebViewClient() {
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
binding.loginProgress.hide()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onReceivedError(
|
override fun onReceivedError(
|
||||||
view: WebView?,
|
view: WebView,
|
||||||
request: WebResourceRequest?,
|
request: WebResourceRequest,
|
||||||
error: WebResourceError
|
error: WebResourceError
|
||||||
) {
|
) {
|
||||||
Log.d("LoginWeb", "Failed to load ${data.url}: $error")
|
Log.d("LoginWeb", "Failed to load ${data.url}: $error")
|
||||||
finish()
|
sendResult(LoginResult.Err(getString(R.string.error_could_not_load_login_page)))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shouldOverrideUrlLoading(
|
override fun shouldOverrideUrlLoading(
|
||||||
view: WebView,
|
view: WebView,
|
||||||
request: WebResourceRequest
|
request: WebResourceRequest
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val url = request.url
|
return shouldOverrideUrlLoading(request.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* overriding this deprecated method is necessary for it to work on api levels < 24 */
|
||||||
|
@Suppress("OVERRIDE_DEPRECATION")
|
||||||
|
override fun shouldOverrideUrlLoading(view: WebView?, urlString: String?): Boolean {
|
||||||
|
val url = urlString?.toUri() ?: return false
|
||||||
|
return shouldOverrideUrlLoading(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun shouldOverrideUrlLoading(url: Uri): Boolean {
|
||||||
return if (url.scheme == oauthUrl.scheme && url.host == oauthUrl.host) {
|
return if (url.scheme == oauthUrl.scheme && url.host == oauthUrl.host) {
|
||||||
val error = url.getQueryParameter("error")
|
val error = url.getQueryParameter("error")
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
|
@ -130,6 +149,7 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
webView.setBackgroundColor(Color.TRANSPARENT)
|
webView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
|
@ -153,10 +173,14 @@ class LoginWebViewActivity : BaseActivity(), Injectable {
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun finish() {
|
||||||
|
super.finishWithoutSlideOutAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
override fun requiresLogin() = false
|
override fun requiresLogin() = false
|
||||||
|
|
||||||
private fun sendResult(result: LoginResult) {
|
private fun sendResult(result: LoginResult) {
|
||||||
setResult(Activity.RESULT_OK, OauthLogin.makeResultIntent(result))
|
setResult(Activity.RESULT_OK, OauthLogin.makeResultIntent(result))
|
||||||
finish()
|
finishWithoutSlideOutAnimation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,9 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.notifications;
|
package com.keylesspalace.tusky.components.notifications;
|
||||||
|
|
||||||
|
import static com.keylesspalace.tusky.util.StatusParsingHelper.parseAsMastodonHtml;
|
||||||
|
import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
|
||||||
|
|
||||||
import android.app.NotificationChannel;
|
import android.app.NotificationChannel;
|
||||||
import android.app.NotificationChannelGroup;
|
import android.app.NotificationChannelGroup;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
|
@ -73,8 +76,6 @@ import java.util.concurrent.TimeUnit;
|
||||||
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 static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
|
|
||||||
|
|
||||||
public class NotificationHelper {
|
public class NotificationHelper {
|
||||||
|
|
||||||
private static int notificationId = 0;
|
private static int notificationId = 0;
|
||||||
|
@ -116,6 +117,8 @@ public class NotificationHelper {
|
||||||
public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE";
|
public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE";
|
||||||
public static final String CHANNEL_POLL = "CHANNEL_POLL";
|
public static final String CHANNEL_POLL = "CHANNEL_POLL";
|
||||||
public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS";
|
public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS";
|
||||||
|
public static final String CHANNEL_SIGN_UP = "CHANNEL_SIGN_UP";
|
||||||
|
public static final String CHANNEL_UPDATES = "CHANNEL_UPDATES";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WorkManager Tag
|
* WorkManager Tag
|
||||||
|
@ -340,7 +343,7 @@ public class NotificationHelper {
|
||||||
Status status = body.getStatus();
|
Status status = body.getStatus();
|
||||||
|
|
||||||
String citedLocalAuthor = status.getAccount().getLocalUsername();
|
String citedLocalAuthor = status.getAccount().getLocalUsername();
|
||||||
String citedText = status.getContent().toString();
|
String citedText = parseAsMastodonHtml(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();
|
||||||
|
@ -392,6 +395,8 @@ public class NotificationHelper {
|
||||||
CHANNEL_FAVOURITE + account.getIdentifier(),
|
CHANNEL_FAVOURITE + account.getIdentifier(),
|
||||||
CHANNEL_POLL + account.getIdentifier(),
|
CHANNEL_POLL + account.getIdentifier(),
|
||||||
CHANNEL_SUBSCRIPTIONS + account.getIdentifier(),
|
CHANNEL_SUBSCRIPTIONS + account.getIdentifier(),
|
||||||
|
CHANNEL_SIGN_UP + account.getIdentifier(),
|
||||||
|
CHANNEL_UPDATES + account.getIdentifier(),
|
||||||
};
|
};
|
||||||
int[] channelNames = {
|
int[] channelNames = {
|
||||||
R.string.notification_mention_name,
|
R.string.notification_mention_name,
|
||||||
|
@ -401,6 +406,8 @@ public class NotificationHelper {
|
||||||
R.string.notification_favourite_name,
|
R.string.notification_favourite_name,
|
||||||
R.string.notification_poll_name,
|
R.string.notification_poll_name,
|
||||||
R.string.notification_subscription_name,
|
R.string.notification_subscription_name,
|
||||||
|
R.string.notification_sign_up_name,
|
||||||
|
R.string.notification_update_name,
|
||||||
};
|
};
|
||||||
int[] channelDescriptions = {
|
int[] channelDescriptions = {
|
||||||
R.string.notification_mention_descriptions,
|
R.string.notification_mention_descriptions,
|
||||||
|
@ -410,6 +417,8 @@ public class NotificationHelper {
|
||||||
R.string.notification_favourite_description,
|
R.string.notification_favourite_description,
|
||||||
R.string.notification_poll_description,
|
R.string.notification_poll_description,
|
||||||
R.string.notification_subscription_description,
|
R.string.notification_subscription_description,
|
||||||
|
R.string.notification_sign_up_description,
|
||||||
|
R.string.notification_update_description,
|
||||||
};
|
};
|
||||||
|
|
||||||
List<NotificationChannel> channels = new ArrayList<>(6);
|
List<NotificationChannel> channels = new ArrayList<>(6);
|
||||||
|
@ -560,6 +569,10 @@ public class NotificationHelper {
|
||||||
return account.getNotificationsFavorited();
|
return account.getNotificationsFavorited();
|
||||||
case POLL:
|
case POLL:
|
||||||
return account.getNotificationsPolls();
|
return account.getNotificationsPolls();
|
||||||
|
case SIGN_UP:
|
||||||
|
return account.getNotificationsSignUps();
|
||||||
|
case UPDATE:
|
||||||
|
return account.getNotificationsUpdates();
|
||||||
default:
|
default:
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -582,6 +595,8 @@ public class NotificationHelper {
|
||||||
return CHANNEL_FAVOURITE + account.getIdentifier();
|
return CHANNEL_FAVOURITE + account.getIdentifier();
|
||||||
case POLL:
|
case POLL:
|
||||||
return CHANNEL_POLL + account.getIdentifier();
|
return CHANNEL_POLL + account.getIdentifier();
|
||||||
|
case SIGN_UP:
|
||||||
|
return CHANNEL_SIGN_UP + account.getIdentifier();
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -663,6 +678,10 @@ public class NotificationHelper {
|
||||||
} else {
|
} else {
|
||||||
return context.getString(R.string.poll_ended_voted);
|
return context.getString(R.string.poll_ended_voted);
|
||||||
}
|
}
|
||||||
|
case SIGN_UP:
|
||||||
|
return String.format(context.getString(R.string.notification_sign_up_format), accountName);
|
||||||
|
case UPDATE:
|
||||||
|
return String.format(context.getString(R.string.notification_update_format), accountName);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -671,6 +690,7 @@ public class NotificationHelper {
|
||||||
switch (notification.getType()) {
|
switch (notification.getType()) {
|
||||||
case FOLLOW:
|
case FOLLOW:
|
||||||
case FOLLOW_REQUEST:
|
case FOLLOW_REQUEST:
|
||||||
|
case SIGN_UP:
|
||||||
return "@" + notification.getAccount().getUsername();
|
return "@" + notification.getAccount().getUsername();
|
||||||
case MENTION:
|
case MENTION:
|
||||||
case FAVOURITE:
|
case FAVOURITE:
|
||||||
|
@ -679,13 +699,13 @@ public class NotificationHelper {
|
||||||
if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) {
|
if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) {
|
||||||
return notification.getStatus().getSpoilerText();
|
return notification.getStatus().getSpoilerText();
|
||||||
} else {
|
} else {
|
||||||
return notification.getStatus().getContent().toString();
|
return parseAsMastodonHtml(notification.getStatus().getContent()).toString();
|
||||||
}
|
}
|
||||||
case POLL:
|
case POLL:
|
||||||
if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) {
|
if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) {
|
||||||
return notification.getStatus().getSpoilerText();
|
return notification.getStatus().getSpoilerText();
|
||||||
} else {
|
} else {
|
||||||
StringBuilder builder = new StringBuilder(notification.getStatus().getContent());
|
StringBuilder builder = new StringBuilder(parseAsMastodonHtml(notification.getStatus().getContent()));
|
||||||
builder.append('\n');
|
builder.append('\n');
|
||||||
Poll poll = notification.getStatus().getPoll();
|
Poll poll = notification.getStatus().getPoll();
|
||||||
List<PollOption> options = poll.getOptions();
|
List<PollOption> options = poll.getOptions();
|
||||||
|
|
|
@ -1,240 +0,0 @@
|
||||||
package com.keylesspalace.tusky.components.preference
|
|
||||||
|
|
||||||
import android.app.AlarmManager
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.widget.RadioButton
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.keylesspalace.tusky.R
|
|
||||||
import com.keylesspalace.tusky.SplashActivity
|
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
|
||||||
import com.keylesspalace.tusky.databinding.DialogEmojicompatBinding
|
|
||||||
import com.keylesspalace.tusky.databinding.ItemEmojiPrefBinding
|
|
||||||
import com.keylesspalace.tusky.util.EmojiCompatFont
|
|
||||||
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.BLOBMOJI
|
|
||||||
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.FONTS
|
|
||||||
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.NOTOEMOJI
|
|
||||||
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.SYSTEM_DEFAULT
|
|
||||||
import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.TWEMOJI
|
|
||||||
import com.keylesspalace.tusky.util.hide
|
|
||||||
import com.keylesspalace.tusky.util.show
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.rxjava3.disposables.Disposable
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import kotlin.system.exitProcess
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This Preference lets the user select their preferred emoji font
|
|
||||||
*/
|
|
||||||
class EmojiPreference(
|
|
||||||
context: Context,
|
|
||||||
private val okHttpClient: OkHttpClient
|
|
||||||
) : Preference(context) {
|
|
||||||
|
|
||||||
private lateinit var selected: EmojiCompatFont
|
|
||||||
private lateinit var original: EmojiCompatFont
|
|
||||||
private val radioButtons = mutableListOf<RadioButton>()
|
|
||||||
private var updated = false
|
|
||||||
private var currentNeedsUpdate = false
|
|
||||||
|
|
||||||
private val downloadDisposables = MutableList<Disposable?>(FONTS.size) { null }
|
|
||||||
|
|
||||||
override fun onAttachedToHierarchy(preferenceManager: PreferenceManager) {
|
|
||||||
super.onAttachedToHierarchy(preferenceManager)
|
|
||||||
|
|
||||||
// Find out which font is currently active
|
|
||||||
selected = EmojiCompatFont.byId(
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0)
|
|
||||||
)
|
|
||||||
// We'll use this later to determine if anything has changed
|
|
||||||
original = selected
|
|
||||||
summary = selected.getDisplay(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick() {
|
|
||||||
val binding = DialogEmojicompatBinding.inflate(LayoutInflater.from(context))
|
|
||||||
|
|
||||||
setupItem(BLOBMOJI, binding.itemBlobmoji)
|
|
||||||
setupItem(TWEMOJI, binding.itemTwemoji)
|
|
||||||
setupItem(NOTOEMOJI, binding.itemNotoemoji)
|
|
||||||
setupItem(SYSTEM_DEFAULT, binding.itemNomoji)
|
|
||||||
|
|
||||||
AlertDialog.Builder(context)
|
|
||||||
.setView(binding.root)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() }
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
|
|
||||||
// Initialize all the views
|
|
||||||
binding.emojiName.text = font.getDisplay(context)
|
|
||||||
binding.emojiCaption.setText(font.caption)
|
|
||||||
binding.emojiThumbnail.setImageResource(font.img)
|
|
||||||
|
|
||||||
// There needs to be a list of all the radio buttons in order to uncheck them when one is selected
|
|
||||||
radioButtons.add(binding.emojiRadioButton)
|
|
||||||
updateItem(font, binding)
|
|
||||||
|
|
||||||
// Set actions
|
|
||||||
binding.emojiDownload.setOnClickListener { startDownload(font, binding) }
|
|
||||||
binding.emojiDownloadCancel.setOnClickListener { cancelDownload(font, binding) }
|
|
||||||
binding.emojiRadioButton.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) }
|
|
||||||
binding.root.setOnClickListener {
|
|
||||||
select(font, binding.emojiRadioButton)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
|
|
||||||
// Switch to downloading style
|
|
||||||
binding.emojiDownload.hide()
|
|
||||||
binding.emojiCaption.visibility = View.INVISIBLE
|
|
||||||
binding.emojiProgress.show()
|
|
||||||
binding.emojiProgress.progress = 0
|
|
||||||
binding.emojiDownloadCancel.show()
|
|
||||||
font.downloadFontFile(context, okHttpClient)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(
|
|
||||||
{ progress ->
|
|
||||||
// The progress is returned as a float between 0 and 1, or -1 if it could not determined
|
|
||||||
if (progress >= 0) {
|
|
||||||
binding.emojiProgress.isIndeterminate = false
|
|
||||||
val max = binding.emojiProgress.max.toFloat()
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
binding.emojiProgress.setProgress((max * progress).toInt(), true)
|
|
||||||
} else {
|
|
||||||
binding.emojiProgress.progress = (max * progress).toInt()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.emojiProgress.isIndeterminate = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show()
|
|
||||||
updateItem(font, binding)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
finishDownload(font, binding)
|
|
||||||
}
|
|
||||||
).also { downloadDisposables[font.id] = it }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancelDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
|
|
||||||
font.deleteDownloadedFile(context)
|
|
||||||
downloadDisposables[font.id]?.dispose()
|
|
||||||
downloadDisposables[font.id] = null
|
|
||||||
updateItem(font, binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun finishDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
|
|
||||||
select(font, binding.emojiRadioButton)
|
|
||||||
updateItem(font, binding)
|
|
||||||
// Set the flag to restart the app (because an update has been downloaded)
|
|
||||||
if (selected === original && currentNeedsUpdate) {
|
|
||||||
updated = true
|
|
||||||
currentNeedsUpdate = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select a font both visually and logically
|
|
||||||
*
|
|
||||||
* @param font The font to be selected
|
|
||||||
* @param radio The radio button associated with it's visual item
|
|
||||||
*/
|
|
||||||
private fun select(font: EmojiCompatFont, radio: RadioButton) {
|
|
||||||
selected = font
|
|
||||||
radioButtons.forEach { radioButton ->
|
|
||||||
radioButton.isChecked = radioButton == radio
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a "consistent" state is reached, i.e. it's not downloading the font
|
|
||||||
*
|
|
||||||
* @param font The font to be displayed
|
|
||||||
* @param binding The ItemEmojiPrefBinding to show the item in
|
|
||||||
*/
|
|
||||||
private fun updateItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) {
|
|
||||||
// There's no download going on
|
|
||||||
binding.emojiProgress.hide()
|
|
||||||
binding.emojiDownloadCancel.hide()
|
|
||||||
binding.emojiCaption.show()
|
|
||||||
if (font.isDownloaded(context)) {
|
|
||||||
// Make it selectable
|
|
||||||
binding.emojiDownload.hide()
|
|
||||||
binding.emojiRadioButton.show()
|
|
||||||
binding.root.isClickable = true
|
|
||||||
} else {
|
|
||||||
// Make it downloadable
|
|
||||||
binding.emojiDownload.show()
|
|
||||||
binding.emojiRadioButton.hide()
|
|
||||||
binding.root.isClickable = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select it if necessary
|
|
||||||
if (font === selected) {
|
|
||||||
binding.emojiRadioButton.isChecked = true
|
|
||||||
// Update available
|
|
||||||
if (!font.isDownloaded(context)) {
|
|
||||||
currentNeedsUpdate = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.emojiRadioButton.isChecked = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveSelectedFont() {
|
|
||||||
val index = selected.id
|
|
||||||
Log.i(TAG, "saveSelectedFont: Font ID: $index")
|
|
||||||
PreferenceManager
|
|
||||||
.getDefaultSharedPreferences(context)
|
|
||||||
.edit()
|
|
||||||
.putInt(key, index)
|
|
||||||
.apply()
|
|
||||||
summary = selected.getDisplay(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User clicked ok -> save the selected font and offer to restart the app if something changed
|
|
||||||
*/
|
|
||||||
private fun onDialogOk() {
|
|
||||||
saveSelectedFont()
|
|
||||||
if (selected !== original || updated) {
|
|
||||||
AlertDialog.Builder(context)
|
|
||||||
.setTitle(R.string.restart_required)
|
|
||||||
.setMessage(R.string.restart_emoji)
|
|
||||||
.setNegativeButton(R.string.later, null)
|
|
||||||
.setPositiveButton(R.string.restart) { _, _ ->
|
|
||||||
// Restart the app
|
|
||||||
// From https://stackoverflow.com/a/17166729/5070653
|
|
||||||
val launchIntent = Intent(context, SplashActivity::class.java)
|
|
||||||
val mPendingIntent = PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
0x1f973, // This is the codepoint of the party face emoji :D
|
|
||||||
launchIntent,
|
|
||||||
NotificationHelper.pendingIntentFlags(false)
|
|
||||||
)
|
|
||||||
val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
|
||||||
mgr.set(
|
|
||||||
AlarmManager.RTC,
|
|
||||||
System.currentTimeMillis() + 100,
|
|
||||||
mPendingIntent
|
|
||||||
)
|
|
||||||
exitProcess(0)
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "EmojiPreference"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -122,6 +122,28 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switchPreference {
|
||||||
|
setTitle(R.string.pref_title_notification_filter_sign_ups)
|
||||||
|
key = PrefKeys.NOTIFICATION_FILTER_SIGN_UPS
|
||||||
|
isIconSpaceReserved = false
|
||||||
|
isChecked = activeAccount.notificationsSignUps
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
updateAccount { it.notificationsSignUps = newValue as Boolean }
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switchPreference {
|
||||||
|
setTitle(R.string.pref_title_notification_filter_updates)
|
||||||
|
key = PrefKeys.NOTIFICATION_FILTER_UPDATES
|
||||||
|
isIconSpaceReserved = false
|
||||||
|
isChecked = activeAccount.notificationsUpdates
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
updateAccount { it.notificationsUpdates = newValue as Boolean }
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
preferenceCategory(R.string.pref_title_notification_alerts) { category ->
|
preferenceCategory(R.string.pref_title_notification_alerts) { category ->
|
||||||
|
|
|
@ -38,14 +38,11 @@ import com.mikepenz.iconics.IconicsDrawable
|
||||||
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
|
||||||
import com.mikepenz.iconics.utils.colorInt
|
import com.mikepenz.iconics.utils.colorInt
|
||||||
import com.mikepenz.iconics.utils.sizePx
|
import com.mikepenz.iconics.utils.sizePx
|
||||||
import okhttp3.OkHttpClient
|
import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
|
|
||||||
@Inject
|
|
||||||
lateinit var okhttpclient: OkHttpClient
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var accountManager: AccountManager
|
lateinit var accountManager: AccountManager
|
||||||
|
|
||||||
|
@ -65,11 +62,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
icon = makeIcon(GoogleMaterial.Icon.gmd_palette)
|
icon = makeIcon(GoogleMaterial.Icon.gmd_palette)
|
||||||
}
|
}
|
||||||
|
|
||||||
emojiPreference(okhttpclient) {
|
emojiPreference(requireActivity()) {
|
||||||
setDefaultValue("system_default")
|
|
||||||
setIcon(R.drawable.ic_emoji_24dp)
|
|
||||||
key = PrefKeys.EMOJI
|
|
||||||
setSummary(R.string.system_default)
|
|
||||||
setTitle(R.string.emoji_style)
|
setTitle(R.string.emoji_style)
|
||||||
icon = makeIcon(GoogleMaterial.Icon.gmd_sentiment_satisfied)
|
icon = makeIcon(GoogleMaterial.Icon.gmd_sentiment_satisfied)
|
||||||
}
|
}
|
||||||
|
@ -300,6 +293,12 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||||
|
if (!EmojiPickerPreference.onDisplayPreferenceDialog(this, preference)) {
|
||||||
|
super.onDisplayPreferenceDialog(preference)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun newInstance(): PreferencesFragment {
|
fun newInstance(): PreferencesFragment {
|
||||||
return PreferencesFragment()
|
return PreferencesFragment()
|
||||||
|
|
|
@ -21,6 +21,7 @@ import androidx.lifecycle.viewModelScope
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
|
import androidx.paging.map
|
||||||
import com.keylesspalace.tusky.appstore.BlockEvent
|
import com.keylesspalace.tusky.appstore.BlockEvent
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.MuteEvent
|
import com.keylesspalace.tusky.appstore.MuteEvent
|
||||||
|
@ -34,11 +35,13 @@ import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Resource
|
import com.keylesspalace.tusky.util.Resource
|
||||||
import com.keylesspalace.tusky.util.RxAwareViewModel
|
import com.keylesspalace.tusky.util.RxAwareViewModel
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
|
import com.keylesspalace.tusky.util.toViewData
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -74,6 +77,11 @@ class ReportViewModel @Inject constructor(
|
||||||
pagingSourceFactory = { StatusesPagingSource(accountId, mastodonApi) }
|
pagingSourceFactory = { StatusesPagingSource(accountId, mastodonApi) }
|
||||||
).flow
|
).flow
|
||||||
}
|
}
|
||||||
|
.map { pagingData ->
|
||||||
|
/* TODO: refactor reports to use the isShowingContent / isExpanded / isCollapsed attributes from StatusViewData.Concrete
|
||||||
|
instead of StatusViewState */
|
||||||
|
pagingData.map { status -> status.toViewData(false, false, false) }
|
||||||
|
}
|
||||||
.cachedIn(viewModelScope)
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
private val selectedIds = HashSet<String>()
|
private val selectedIds = HashSet<String>()
|
||||||
|
@ -155,7 +163,7 @@ class ReportViewModel @Inject constructor(
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{ relationship ->
|
{ relationship ->
|
||||||
val muting = relationship?.muting == true
|
val muting = relationship.muting
|
||||||
muteStateMutable.value = Success(muting)
|
muteStateMutable.value = Success(muting)
|
||||||
if (muting) {
|
if (muting) {
|
||||||
eventHub.dispatch(MuteEvent(accountId))
|
eventHub.dispatch(MuteEvent(accountId))
|
||||||
|
@ -180,7 +188,7 @@ class ReportViewModel @Inject constructor(
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(
|
.subscribe(
|
||||||
{ relationship ->
|
{ relationship ->
|
||||||
val blocking = relationship?.blocking == true
|
val blocking = relationship.blocking
|
||||||
blockStateMutable.value = Success(blocking)
|
blockStateMutable.value = Success(blocking)
|
||||||
if (blocking) {
|
if (blocking) {
|
||||||
eventHub.dispatch(BlockEvent(accountId))
|
eventHub.dispatch(BlockEvent(accountId))
|
||||||
|
|
|
@ -26,6 +26,7 @@ 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.interfaces.LinkListener
|
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
|
import com.keylesspalace.tusky.util.AbsoluteTimeFormatter
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||||
import com.keylesspalace.tusky.util.StatusViewHelper
|
import com.keylesspalace.tusky.util.StatusViewHelper
|
||||||
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER
|
import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER
|
||||||
|
@ -37,6 +38,7 @@ import com.keylesspalace.tusky.util.setClickableMentions
|
||||||
import com.keylesspalace.tusky.util.setClickableText
|
import com.keylesspalace.tusky.util.setClickableText
|
||||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import com.keylesspalace.tusky.viewdata.toViewData
|
import com.keylesspalace.tusky.viewdata.toViewData
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
@ -45,20 +47,22 @@ class StatusViewHolder(
|
||||||
private val statusDisplayOptions: StatusDisplayOptions,
|
private val statusDisplayOptions: StatusDisplayOptions,
|
||||||
private val viewState: StatusViewState,
|
private val viewState: StatusViewState,
|
||||||
private val adapterHandler: AdapterHandler,
|
private val adapterHandler: AdapterHandler,
|
||||||
private val getStatusForPosition: (Int) -> Status?
|
private val getStatusForPosition: (Int) -> StatusViewData.Concrete?
|
||||||
) : RecyclerView.ViewHolder(binding.root) {
|
) : RecyclerView.ViewHolder(binding.root) {
|
||||||
|
|
||||||
private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height)
|
private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height)
|
||||||
private val statusViewHelper = StatusViewHelper(itemView)
|
private val statusViewHelper = StatusViewHelper(itemView)
|
||||||
|
private val absoluteTimeFormatter = AbsoluteTimeFormatter()
|
||||||
|
|
||||||
private val previewListener = object : StatusViewHelper.MediaPreviewListener {
|
private val previewListener = object : StatusViewHelper.MediaPreviewListener {
|
||||||
override fun onViewMedia(v: View?, idx: Int) {
|
override fun onViewMedia(v: View?, idx: Int) {
|
||||||
status()?.let { status ->
|
viewdata()?.let { viewdata ->
|
||||||
adapterHandler.showMedia(v, status, idx)
|
adapterHandler.showMedia(v, viewdata.status, idx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onContentHiddenChange(isShowing: Boolean) {
|
override fun onContentHiddenChange(isShowing: Boolean) {
|
||||||
status()?.id?.let { id ->
|
viewdata()?.id?.let { id ->
|
||||||
viewState.setMediaShow(id, isShowing)
|
viewState.setMediaShow(id, isShowing)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,57 +70,57 @@ class StatusViewHolder(
|
||||||
|
|
||||||
init {
|
init {
|
||||||
binding.statusSelection.setOnCheckedChangeListener { _, isChecked ->
|
binding.statusSelection.setOnCheckedChangeListener { _, isChecked ->
|
||||||
status()?.let { status ->
|
viewdata()?.let { viewdata ->
|
||||||
adapterHandler.setStatusChecked(status, isChecked)
|
adapterHandler.setStatusChecked(viewdata.status, isChecked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding.statusMediaPreviewContainer.clipToOutline = true
|
binding.statusMediaPreviewContainer.clipToOutline = true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bind(status: Status) {
|
fun bind(viewData: StatusViewData.Concrete) {
|
||||||
binding.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id)
|
binding.statusSelection.isChecked = adapterHandler.isStatusChecked(viewData.id)
|
||||||
|
|
||||||
updateTextView()
|
updateTextView()
|
||||||
|
|
||||||
val sensitive = status.sensitive
|
val sensitive = viewData.status.sensitive
|
||||||
|
|
||||||
statusViewHelper.setMediasPreview(
|
statusViewHelper.setMediasPreview(
|
||||||
statusDisplayOptions, status.attachments,
|
statusDisplayOptions, viewData.status.attachments,
|
||||||
sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive),
|
sensitive, previewListener, viewState.isMediaShow(viewData.id, viewData.status.sensitive),
|
||||||
mediaViewHeight
|
mediaViewHeight
|
||||||
)
|
)
|
||||||
|
|
||||||
statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions)
|
statusViewHelper.setupPollReadonly(viewData.status.poll.toViewData(), viewData.status.emojis, statusDisplayOptions)
|
||||||
setCreatedAt(status.createdAt)
|
setCreatedAt(viewData.status.createdAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTextView() {
|
private fun updateTextView() {
|
||||||
status()?.let { status ->
|
viewdata()?.let { viewdata ->
|
||||||
setupCollapsedState(
|
setupCollapsedState(
|
||||||
shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true),
|
shouldTrimStatus(viewdata.content), viewState.isCollapsed(viewdata.id, true),
|
||||||
viewState.isContentShow(status.id, status.sensitive), status.spoilerText
|
viewState.isContentShow(viewdata.id, viewdata.status.sensitive), viewdata.spoilerText
|
||||||
)
|
)
|
||||||
|
|
||||||
if (status.spoilerText.isBlank()) {
|
if (viewdata.spoilerText.isBlank()) {
|
||||||
setTextVisible(true, status.content, status.mentions, status.tags, status.emojis, adapterHandler)
|
setTextVisible(true, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler)
|
||||||
binding.statusContentWarningButton.hide()
|
binding.statusContentWarningButton.hide()
|
||||||
binding.statusContentWarningDescription.hide()
|
binding.statusContentWarningDescription.hide()
|
||||||
} else {
|
} else {
|
||||||
val emojiSpoiler = status.spoilerText.emojify(status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis)
|
val emojiSpoiler = viewdata.spoilerText.emojify(viewdata.status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis)
|
||||||
binding.statusContentWarningDescription.text = emojiSpoiler
|
binding.statusContentWarningDescription.text = emojiSpoiler
|
||||||
binding.statusContentWarningDescription.show()
|
binding.statusContentWarningDescription.show()
|
||||||
binding.statusContentWarningButton.show()
|
binding.statusContentWarningButton.show()
|
||||||
setContentWarningButtonText(viewState.isContentShow(status.id, true))
|
setContentWarningButtonText(viewState.isContentShow(viewdata.id, true))
|
||||||
binding.statusContentWarningButton.setOnClickListener {
|
binding.statusContentWarningButton.setOnClickListener {
|
||||||
status()?.let { status ->
|
viewdata()?.let { viewdata ->
|
||||||
val contentShown = viewState.isContentShow(status.id, true)
|
val contentShown = viewState.isContentShow(viewdata.id, true)
|
||||||
binding.statusContentWarningDescription.invalidate()
|
binding.statusContentWarningDescription.invalidate()
|
||||||
viewState.setContentShow(status.id, !contentShown)
|
viewState.setContentShow(viewdata.id, !contentShown)
|
||||||
setTextVisible(!contentShown, status.content, status.mentions, status.tags, status.emojis, adapterHandler)
|
setTextVisible(!contentShown, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler)
|
||||||
setContentWarningButtonText(!contentShown)
|
setContentWarningButtonText(!contentShown)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.tags, status.emojis, adapterHandler)
|
setTextVisible(viewState.isContentShow(viewdata.id, true), viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -152,7 +156,7 @@ class StatusViewHolder(
|
||||||
|
|
||||||
private fun setCreatedAt(createdAt: Date?) {
|
private fun setCreatedAt(createdAt: Date?) {
|
||||||
if (statusDisplayOptions.useAbsoluteTime) {
|
if (statusDisplayOptions.useAbsoluteTime) {
|
||||||
binding.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt)
|
binding.timestampInfo.text = absoluteTimeFormatter.format(createdAt)
|
||||||
} else {
|
} else {
|
||||||
binding.timestampInfo.text = if (createdAt != null) {
|
binding.timestampInfo.text = if (createdAt != null) {
|
||||||
val then = createdAt.time
|
val then = createdAt.time
|
||||||
|
@ -169,8 +173,8 @@ class StatusViewHolder(
|
||||||
/* input filter for TextViews have to be set before text */
|
/* input filter for TextViews have to be set before text */
|
||||||
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
|
if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) {
|
||||||
binding.buttonToggleContent.setOnClickListener {
|
binding.buttonToggleContent.setOnClickListener {
|
||||||
status()?.let { status ->
|
viewdata()?.let { viewdata ->
|
||||||
viewState.setCollapsed(status.id, !collapsed)
|
viewState.setCollapsed(viewdata.id, !collapsed)
|
||||||
updateTextView()
|
updateTextView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -189,5 +193,5 @@ class StatusViewHolder(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun status() = getStatusForPosition(bindingAdapterPosition)
|
private fun viewdata() = getStatusForPosition(bindingAdapterPosition)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,16 +22,16 @@ import androidx.recyclerview.widget.DiffUtil
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.components.report.model.StatusViewState
|
import com.keylesspalace.tusky.components.report.model.StatusViewState
|
||||||
import com.keylesspalace.tusky.databinding.ItemReportStatusBinding
|
import com.keylesspalace.tusky.databinding.ItemReportStatusBinding
|
||||||
import com.keylesspalace.tusky.entity.Status
|
|
||||||
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
import com.keylesspalace.tusky.util.StatusDisplayOptions
|
||||||
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
|
||||||
class StatusesAdapter(
|
class StatusesAdapter(
|
||||||
private val statusDisplayOptions: StatusDisplayOptions,
|
private val statusDisplayOptions: StatusDisplayOptions,
|
||||||
private val statusViewState: StatusViewState,
|
private val statusViewState: StatusViewState,
|
||||||
private val adapterHandler: AdapterHandler
|
private val adapterHandler: AdapterHandler
|
||||||
) : PagingDataAdapter<Status, StatusViewHolder>(STATUS_COMPARATOR) {
|
) : PagingDataAdapter<StatusViewData.Concrete, StatusViewHolder>(STATUS_COMPARATOR) {
|
||||||
|
|
||||||
private val statusForPosition: (Int) -> Status? = { position: Int ->
|
private val statusForPosition: (Int) -> StatusViewData.Concrete? = { position: Int ->
|
||||||
if (position != RecyclerView.NO_POSITION) getItem(position) else null
|
if (position != RecyclerView.NO_POSITION) getItem(position) else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,11 +50,11 @@ class StatusesAdapter(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<Status>() {
|
val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<StatusViewData.Concrete>() {
|
||||||
override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean =
|
override fun areContentsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
|
||||||
oldItem == newItem
|
oldItem == newItem
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean =
|
override fun areItemsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean =
|
||||||
oldItem.id == newItem.id
|
oldItem.id == newItem.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,7 +25,6 @@ import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ScheduledStatusViewModel @Inject constructor(
|
class ScheduledStatusViewModel @Inject constructor(
|
||||||
|
@ -43,12 +42,14 @@ class ScheduledStatusViewModel @Inject constructor(
|
||||||
|
|
||||||
fun deleteScheduledStatus(status: ScheduledStatus) {
|
fun deleteScheduledStatus(status: ScheduledStatus) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
try {
|
mastodonApi.deleteScheduledStatus(status.id).fold(
|
||||||
mastodonApi.deleteScheduledStatus(status.id).await()
|
{
|
||||||
pagingSourceFactory.remove(status)
|
pagingSourceFactory.remove(status)
|
||||||
} catch (throwable: Throwable) {
|
},
|
||||||
Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable)
|
{ throwable ->
|
||||||
}
|
Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,6 +84,10 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun finish() {
|
||||||
|
super.finishWithoutSlideOutAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getPageTitle(position: Int): CharSequence {
|
private fun getPageTitle(position: Int): CharSequence {
|
||||||
return when (position) {
|
return when (position) {
|
||||||
0 -> getString(R.string.title_posts)
|
0 -> getString(R.string.title_posts)
|
||||||
|
|
|
@ -111,9 +111,13 @@ abstract class SearchFragment<T : Any> :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id))
|
override fun onViewAccount(id: String) {
|
||||||
|
bottomSheetActivity?.startActivityWithSlideInAnimation(AccountActivity.getIntent(requireContext(), id))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewTag(tag: String) = startActivity(StatusListActivity.newHashtagIntent(requireContext(), tag))
|
override fun onViewTag(tag: String) {
|
||||||
|
bottomSheetActivity?.startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onViewUrl(url: String) {
|
override fun onViewUrl(url: String) {
|
||||||
bottomSheetActivity?.viewUrl(url)
|
bottomSheetActivity?.viewUrl(url)
|
||||||
|
|
|
@ -97,7 +97,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReply(position: Int) {
|
override fun onReply(position: Int) {
|
||||||
searchAdapter.peek(position)?.status?.let { status ->
|
searchAdapter.peek(position)?.let { status ->
|
||||||
reply(status)
|
reply(status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -199,8 +199,8 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
fun newInstance() = SearchStatusesFragment()
|
fun newInstance() = SearchStatusesFragment()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun reply(status: Status) {
|
private fun reply(status: StatusViewData.Concrete) {
|
||||||
val actionableStatus = status.actionableStatus
|
val actionableStatus = status.actionable
|
||||||
val mentionedUsernames = actionableStatus.mentions.map { it.username }
|
val mentionedUsernames = actionableStatus.mentions.map { it.username }
|
||||||
.toMutableSet()
|
.toMutableSet()
|
||||||
.apply {
|
.apply {
|
||||||
|
@ -216,10 +216,10 @@ class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), Status
|
||||||
contentWarning = actionableStatus.spoilerText,
|
contentWarning = actionableStatus.spoilerText,
|
||||||
mentionedUsernames = mentionedUsernames,
|
mentionedUsernames = mentionedUsernames,
|
||||||
replyingStatusAuthor = actionableStatus.account.localUsername,
|
replyingStatusAuthor = actionableStatus.account.localUsername,
|
||||||
replyingStatusContent = actionableStatus.content.toString()
|
replyingStatusContent = status.content.toString()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
startActivity(intent)
|
bottomSheetActivity?.startActivityWithSlideInAnimation(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun more(status: Status, view: View, position: Int) {
|
private fun more(status: Status, view: View, position: Int) {
|
||||||
|
|
|
@ -172,7 +172,7 @@ class TimelineFragment :
|
||||||
setupRecyclerView()
|
setupRecyclerView()
|
||||||
|
|
||||||
adapter.addLoadStateListener { loadState ->
|
adapter.addLoadStateListener { loadState ->
|
||||||
if (loadState.refresh != LoadState.Loading) {
|
if (loadState.refresh != LoadState.Loading && loadState.source.refresh != LoadState.Loading) {
|
||||||
binding.swipeRefreshLayout.isRefreshing = false
|
binding.swipeRefreshLayout.isRefreshing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,22 +15,18 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.timeline
|
package com.keylesspalace.tusky.components.timeline
|
||||||
|
|
||||||
import android.text.SpannedString
|
|
||||||
import androidx.core.text.parseAsHtml
|
|
||||||
import androidx.core.text.toHtml
|
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
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.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
|
import com.keylesspalace.tusky.entity.Card
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
import com.keylesspalace.tusky.entity.HashTag
|
import com.keylesspalace.tusky.entity.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.entity.TimelineAccount
|
||||||
import com.keylesspalace.tusky.util.shouldTrimStatus
|
|
||||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
@ -101,7 +97,8 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
|
||||||
expanded = loading,
|
expanded = loading,
|
||||||
contentCollapsed = false,
|
contentCollapsed = false,
|
||||||
contentShowing = false,
|
contentShowing = false,
|
||||||
pinned = false
|
pinned = false,
|
||||||
|
card = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +116,7 @@ fun Status.toEntity(
|
||||||
authorServerId = actionableStatus.account.id,
|
authorServerId = actionableStatus.account.id,
|
||||||
inReplyToId = actionableStatus.inReplyToId,
|
inReplyToId = actionableStatus.inReplyToId,
|
||||||
inReplyToAccountId = actionableStatus.inReplyToAccountId,
|
inReplyToAccountId = actionableStatus.inReplyToAccountId,
|
||||||
content = actionableStatus.content.toHtml(),
|
content = actionableStatus.content,
|
||||||
createdAt = actionableStatus.createdAt.time,
|
createdAt = actionableStatus.createdAt.time,
|
||||||
emojis = actionableStatus.emojis.let(gson::toJson),
|
emojis = actionableStatus.emojis.let(gson::toJson),
|
||||||
reblogsCount = actionableStatus.reblogsCount,
|
reblogsCount = actionableStatus.reblogsCount,
|
||||||
|
@ -141,7 +138,8 @@ fun Status.toEntity(
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
contentShowing = contentShowing,
|
contentShowing = contentShowing,
|
||||||
contentCollapsed = contentCollapsed,
|
contentCollapsed = contentCollapsed,
|
||||||
pinned = actionableStatus.pinned == true
|
pinned = actionableStatus.pinned == true,
|
||||||
|
card = actionableStatus.card?.let(gson::toJson),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,6 +154,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
val application = gson.fromJson(status.application, Status.Application::class.java)
|
val application = gson.fromJson(status.application, Status.Application::class.java)
|
||||||
val emojis: List<Emoji> = gson.fromJson(status.emojis, emojisListType) ?: emptyList()
|
val emojis: List<Emoji> = gson.fromJson(status.emojis, emojisListType) ?: emptyList()
|
||||||
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
|
val poll: Poll? = gson.fromJson(status.poll, Poll::class.java)
|
||||||
|
val card: Card? = gson.fromJson(status.card, Card::class.java)
|
||||||
|
|
||||||
val reblog = status.reblogServerId?.let { id ->
|
val reblog = status.reblogServerId?.let { id ->
|
||||||
Status(
|
Status(
|
||||||
|
@ -165,8 +164,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
inReplyToId = status.inReplyToId,
|
inReplyToId = status.inReplyToId,
|
||||||
inReplyToAccountId = status.inReplyToAccountId,
|
inReplyToAccountId = status.inReplyToAccountId,
|
||||||
reblog = null,
|
reblog = null,
|
||||||
content = status.content?.parseAsHtml()?.trimTrailingWhitespace()
|
content = status.content.orEmpty(),
|
||||||
?: SpannedString(""),
|
|
||||||
createdAt = Date(status.createdAt),
|
createdAt = Date(status.createdAt),
|
||||||
emojis = emojis,
|
emojis = emojis,
|
||||||
reblogsCount = status.reblogsCount,
|
reblogsCount = status.reblogsCount,
|
||||||
|
@ -184,7 +182,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
pinned = false,
|
pinned = false,
|
||||||
muted = status.muted,
|
muted = status.muted,
|
||||||
poll = poll,
|
poll = poll,
|
||||||
card = null
|
card = card,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val status = if (reblog != null) {
|
val status = if (reblog != null) {
|
||||||
|
@ -195,7 +193,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
inReplyToId = null,
|
inReplyToId = null,
|
||||||
inReplyToAccountId = null,
|
inReplyToAccountId = null,
|
||||||
reblog = reblog,
|
reblog = reblog,
|
||||||
content = SpannedString(""),
|
content = "",
|
||||||
createdAt = Date(status.createdAt), // lie but whatever?
|
createdAt = Date(status.createdAt), // lie but whatever?
|
||||||
emojis = listOf(),
|
emojis = listOf(),
|
||||||
reblogsCount = 0,
|
reblogsCount = 0,
|
||||||
|
@ -223,8 +221,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
inReplyToId = status.inReplyToId,
|
inReplyToId = status.inReplyToId,
|
||||||
inReplyToAccountId = status.inReplyToAccountId,
|
inReplyToAccountId = status.inReplyToAccountId,
|
||||||
reblog = null,
|
reblog = null,
|
||||||
content = status.content?.parseAsHtml()?.trimTrailingWhitespace()
|
content = status.content.orEmpty(),
|
||||||
?: SpannedString(""),
|
|
||||||
createdAt = Date(status.createdAt),
|
createdAt = Date(status.createdAt),
|
||||||
emojis = emojis,
|
emojis = emojis,
|
||||||
reblogsCount = status.reblogsCount,
|
reblogsCount = status.reblogsCount,
|
||||||
|
@ -242,14 +239,13 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
|
||||||
pinned = status.pinned,
|
pinned = status.pinned,
|
||||||
muted = status.muted,
|
muted = status.muted,
|
||||||
poll = poll,
|
poll = poll,
|
||||||
card = null
|
card = card,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return StatusViewData.Concrete(
|
return StatusViewData.Concrete(
|
||||||
status = status,
|
status = status,
|
||||||
isExpanded = this.status.expanded,
|
isExpanded = this.status.expanded,
|
||||||
isShowingContent = this.status.contentShowing,
|
isShowingContent = this.status.contentShowing,
|
||||||
isCollapsible = shouldTrimStatus(status.content),
|
|
||||||
isCollapsed = this.status.contentCollapsed
|
isCollapsed = this.status.contentCollapsed
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,7 +42,10 @@ 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.viewdata.StatusViewData
|
import com.keylesspalace.tusky.viewdata.StatusViewData
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.asExecutor
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
|
@ -79,15 +82,13 @@ class CachedTimelineViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
).flow
|
).flow
|
||||||
.map { pagingData ->
|
.map { pagingData ->
|
||||||
pagingData.map { timelineStatus ->
|
pagingData.map(Dispatchers.Default.asExecutor()) { timelineStatus ->
|
||||||
timelineStatus.toViewData(gson)
|
timelineStatus.toViewData(gson)
|
||||||
}
|
}.filter(Dispatchers.Default.asExecutor()) { statusViewData ->
|
||||||
}
|
|
||||||
.map { pagingData ->
|
|
||||||
pagingData.filter { statusViewData ->
|
|
||||||
!shouldFilterStatus(statusViewData)
|
!shouldFilterStatus(statusViewData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.flowOn(Dispatchers.Default)
|
||||||
.cachedIn(viewModelScope)
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
|
@ -40,6 +40,9 @@ import com.keylesspalace.tusky.util.isLessThan
|
||||||
import com.keylesspalace.tusky.util.isLessThanOrEqual
|
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.Dispatchers
|
||||||
|
import kotlinx.coroutines.asExecutor
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.rx3.await
|
import kotlinx.coroutines.rx3.await
|
||||||
|
@ -79,10 +82,11 @@ class NetworkTimelineViewModel @Inject constructor(
|
||||||
remoteMediator = NetworkTimelineRemoteMediator(accountManager, this)
|
remoteMediator = NetworkTimelineRemoteMediator(accountManager, this)
|
||||||
).flow
|
).flow
|
||||||
.map { pagingData ->
|
.map { pagingData ->
|
||||||
pagingData.filter { statusViewData ->
|
pagingData.filter(Dispatchers.Default.asExecutor()) { statusViewData ->
|
||||||
!shouldFilterStatus(statusViewData)
|
!shouldFilterStatus(statusViewData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.flowOn(Dispatchers.Default)
|
||||||
.cachedIn(viewModelScope)
|
.cachedIn(viewModelScope)
|
||||||
|
|
||||||
override fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) {
|
override fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) {
|
||||||
|
|
|
@ -50,6 +50,8 @@ data class AccountEntity(
|
||||||
var notificationsFavorited: Boolean = true,
|
var notificationsFavorited: Boolean = true,
|
||||||
var notificationsPolls: Boolean = true,
|
var notificationsPolls: Boolean = true,
|
||||||
var notificationsSubscriptions: Boolean = true,
|
var notificationsSubscriptions: Boolean = true,
|
||||||
|
var notificationsSignUps: Boolean = true,
|
||||||
|
var notificationsUpdates: Boolean = true,
|
||||||
var notificationSound: Boolean = true,
|
var notificationSound: Boolean = true,
|
||||||
var notificationVibration: Boolean = true,
|
var notificationVibration: Boolean = true,
|
||||||
var notificationLight: Boolean = true,
|
var notificationLight: Boolean = true,
|
||||||
|
|
|
@ -31,7 +31,7 @@ import java.io.File;
|
||||||
*/
|
*/
|
||||||
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||||
TimelineAccountEntity.class, ConversationEntity.class
|
TimelineAccountEntity.class, ConversationEntity.class
|
||||||
}, version = 31)
|
}, version = 35)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public abstract AccountDao accountDao();
|
public abstract AccountDao accountDao();
|
||||||
|
@ -483,4 +483,62 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
database.execSQL("DELETE FROM `TimelineStatusEntity`");
|
database.execSQL("DELETE FROM `TimelineStatusEntity`");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_31_32 = new Migration(31, 32) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSignUps` INTEGER NOT NULL DEFAULT 1");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_32_33 = new Migration(32, 33) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
|
||||||
|
// ConversationEntity lost the s_collapsible column
|
||||||
|
// since SQLite does not support removing columns and it is just a cache table, we recreate the whole table.
|
||||||
|
database.execSQL("DROP TABLE `ConversationEntity`");
|
||||||
|
database.execSQL("CREATE TABLE IF NOT EXISTS `ConversationEntity` (" +
|
||||||
|
"`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_collapsed` INTEGER NOT NULL," +
|
||||||
|
"`s_muted` INTEGER NOT NULL," +
|
||||||
|
"`s_poll` TEXT," +
|
||||||
|
"PRIMARY KEY(`id`, `accountId`))");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_33_34 = new Migration(33, 34) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsUpdates` INTEGER NOT NULL DEFAULT 1");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_34_35 = new Migration(34, 35) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `card` TEXT");
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,7 +17,6 @@ package com.keylesspalace.tusky.db
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import androidx.room.Dao
|
import androidx.room.Dao
|
||||||
import androidx.room.Delete
|
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
|
@ -31,8 +30,8 @@ interface ConversationsDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun insert(conversation: ConversationEntity): Long
|
suspend fun insert(conversation: ConversationEntity): Long
|
||||||
|
|
||||||
@Delete
|
@Query("DELETE FROM ConversationEntity WHERE id = :id AND accountId = :accountId")
|
||||||
suspend fun delete(conversation: ConversationEntity): Int
|
suspend fun delete(id: String, accountId: Long): Int
|
||||||
|
|
||||||
@Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC")
|
@Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC")
|
||||||
fun conversationsForAccount(accountId: Long): PagingSource<Int, ConversationEntity>
|
fun conversationsForAccount(accountId: Long): PagingSource<Int, ConversationEntity>
|
||||||
|
|
|
@ -15,9 +15,6 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.db
|
package com.keylesspalace.tusky.db
|
||||||
|
|
||||||
import android.text.Spanned
|
|
||||||
import androidx.core.text.parseAsHtml
|
|
||||||
import androidx.core.text.toHtml
|
|
||||||
import androidx.room.ProvidedTypeConverter
|
import androidx.room.ProvidedTypeConverter
|
||||||
import androidx.room.TypeConverter
|
import androidx.room.TypeConverter
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
@ -31,10 +28,8 @@ import com.keylesspalace.tusky.entity.HashTag
|
||||||
import com.keylesspalace.tusky.entity.NewPoll
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
import com.keylesspalace.tusky.entity.Poll
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.util.ArrayList
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
@ -140,22 +135,6 @@ class Converters @Inject constructor (
|
||||||
return Date(date)
|
return Date(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TypeConverter
|
|
||||||
fun spannedToString(spanned: Spanned?): String? {
|
|
||||||
if (spanned == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return spanned.toHtml()
|
|
||||||
}
|
|
||||||
|
|
||||||
@TypeConverter
|
|
||||||
fun stringToSpanned(spannedString: String?): Spanned? {
|
|
||||||
if (spannedString == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return spannedString.parseAsHtml().trimTrailingWhitespace()
|
|
||||||
}
|
|
||||||
|
|
||||||
@TypeConverter
|
@TypeConverter
|
||||||
fun pollToJson(poll: Poll?): String? {
|
fun pollToJson(poll: Poll?): String? {
|
||||||
return gson.toJson(poll)
|
return gson.toJson(poll)
|
||||||
|
|
|
@ -19,13 +19,19 @@ import androidx.room.Dao
|
||||||
import androidx.room.Insert
|
import androidx.room.Insert
|
||||||
import androidx.room.OnConflictStrategy
|
import androidx.room.OnConflictStrategy
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import io.reactivex.rxjava3.core.Single
|
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface InstanceDao {
|
interface InstanceDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
fun insertOrReplace(instance: InstanceEntity)
|
@Insert(onConflict = OnConflictStrategy.REPLACE, entity = InstanceEntity::class)
|
||||||
|
suspend fun insertOrReplace(instance: InstanceInfoEntity)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE, entity = InstanceEntity::class)
|
||||||
|
suspend fun insertOrReplace(emojis: EmojisEntity)
|
||||||
|
|
||||||
@Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1")
|
@Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1")
|
||||||
fun loadMetadataForInstance(instance: String): Single<InstanceEntity>
|
suspend fun getInstanceInfo(instance: String): InstanceInfoEntity?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1")
|
||||||
|
suspend fun getEmojiInfo(instance: String): EmojisEntity?
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ import com.keylesspalace.tusky.entity.Emoji
|
||||||
@Entity
|
@Entity
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
data class InstanceEntity(
|
data class InstanceEntity(
|
||||||
@field:PrimaryKey var instance: String,
|
@PrimaryKey val instance: String,
|
||||||
val emojiList: List<Emoji>?,
|
val emojiList: List<Emoji>?,
|
||||||
val maximumTootCharacters: Int?,
|
val maximumTootCharacters: Int?,
|
||||||
val maxPollOptions: Int?,
|
val maxPollOptions: Int?,
|
||||||
|
@ -33,3 +33,20 @@ data class InstanceEntity(
|
||||||
val charactersReservedPerUrl: Int?,
|
val charactersReservedPerUrl: Int?,
|
||||||
val version: String?
|
val version: String?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
|
data class EmojisEntity(
|
||||||
|
@PrimaryKey val instance: String,
|
||||||
|
val emojiList: List<Emoji>?
|
||||||
|
)
|
||||||
|
|
||||||
|
data class InstanceInfoEntity(
|
||||||
|
@PrimaryKey val instance: String,
|
||||||
|
val maximumTootCharacters: Int?,
|
||||||
|
val maxPollOptions: Int?,
|
||||||
|
val maxPollOptionLength: Int?,
|
||||||
|
val minPollDuration: Int?,
|
||||||
|
val maxPollDuration: Int?,
|
||||||
|
val charactersReservedPerUrl: Int?,
|
||||||
|
val version: String?
|
||||||
|
)
|
||||||
|
|
|
@ -36,7 +36,7 @@ SELECT s.serverId, s.url, s.timelineUserId,
|
||||||
s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt,
|
s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt,
|
||||||
s.emojis, s.reblogsCount, s.favouritesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
|
s.emojis, s.reblogsCount, s.favouritesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
|
||||||
s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId,
|
s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId,
|
||||||
s.content, s.attachments, s.poll, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned,
|
s.content, s.attachments, s.poll, s.card, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned,
|
||||||
a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId',
|
a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId',
|
||||||
a.localUsername as 'a_localUsername', a.username as 'a_username',
|
a.localUsername as 'a_localUsername', a.username as 'a_username',
|
||||||
a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar',
|
a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar',
|
||||||
|
|
|
@ -78,7 +78,8 @@ data class TimelineStatusEntity(
|
||||||
val expanded: Boolean, // used as the "loading" attribute when this TimelineStatusEntity is a placeholder
|
val expanded: Boolean, // used as the "loading" attribute when this TimelineStatusEntity is a placeholder
|
||||||
val contentCollapsed: Boolean,
|
val contentCollapsed: Boolean,
|
||||||
val contentShowing: Boolean,
|
val contentShowing: Boolean,
|
||||||
val pinned: Boolean
|
val pinned: Boolean,
|
||||||
|
val card: String?,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
|
|
|
@ -62,7 +62,8 @@ 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_30_31
|
AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32,
|
||||||
|
AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35,
|
||||||
)
|
)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,14 +18,13 @@ package com.keylesspalace.tusky.di
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.Spanned
|
import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.GsonBuilder
|
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.json.SpannedTypeAdapter
|
|
||||||
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
|
import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.network.MediaUploadApi
|
||||||
import com.keylesspalace.tusky.util.getNonNullString
|
import com.keylesspalace.tusky.util.getNonNullString
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
import dagger.Provides
|
import dagger.Provides
|
||||||
|
@ -51,11 +50,7 @@ class NetworkModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesGson(): Gson {
|
fun providesGson() = Gson()
|
||||||
return GsonBuilder()
|
|
||||||
.registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter())
|
|
||||||
.create()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
|
@ -111,10 +106,25 @@ class NetworkModule {
|
||||||
.client(httpClient)
|
.client(httpClient)
|
||||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||||
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
|
.addCallAdapterFactory(RxJava3CallAdapterFactory.create())
|
||||||
|
.addCallAdapterFactory(KotlinResultCallAdapterFactory.create())
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesApi(retrofit: Retrofit): MastodonApi = retrofit.create()
|
fun providesApi(retrofit: Retrofit): MastodonApi = retrofit.create()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesMediaUploadApi(retrofit: Retrofit, okHttpClient: OkHttpClient): MediaUploadApi {
|
||||||
|
val longTimeOutOkHttpClient = okHttpClient.newBuilder()
|
||||||
|
.readTimeout(100, TimeUnit.SECONDS)
|
||||||
|
.writeTimeout(100, TimeUnit.SECONDS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
return retrofit.newBuilder()
|
||||||
|
.client(longTimeOutOkHttpClient)
|
||||||
|
.build()
|
||||||
|
.create()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
import android.text.Spanned
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
@ -24,7 +23,7 @@ data class Account(
|
||||||
@SerializedName("username") val localUsername: String,
|
@SerializedName("username") val localUsername: String,
|
||||||
@SerializedName("acct") val username: 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
|
@SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract
|
||||||
val note: Spanned,
|
val note: String,
|
||||||
val url: String,
|
val url: String,
|
||||||
val avatar: String,
|
val avatar: String,
|
||||||
val header: String,
|
val header: String,
|
||||||
|
@ -46,56 +45,6 @@ data class Account(
|
||||||
} else displayName
|
} else displayName
|
||||||
|
|
||||||
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(
|
||||||
|
@ -107,7 +56,7 @@ data class AccountSource(
|
||||||
|
|
||||||
data class Field(
|
data class Field(
|
||||||
val name: String,
|
val name: String,
|
||||||
val value: Spanned,
|
val value: String,
|
||||||
@SerializedName("verified_at") val verifiedAt: Date?
|
@SerializedName("verified_at") val verifiedAt: Date?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -15,13 +15,12 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
import android.text.Spanned
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
data class Announcement(
|
data class Announcement(
|
||||||
val id: String,
|
val id: String,
|
||||||
val content: Spanned,
|
val content: String,
|
||||||
@SerializedName("starts_at") val startsAt: Date?,
|
@SerializedName("starts_at") val startsAt: Date?,
|
||||||
@SerializedName("ends_at") val endsAt: Date?,
|
@SerializedName("ends_at") val endsAt: Date?,
|
||||||
@SerializedName("all_day") val allDay: Boolean,
|
@SerializedName("all_day") val allDay: Boolean,
|
||||||
|
|
|
@ -15,13 +15,12 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
import android.text.Spanned
|
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
data class Card(
|
data class Card(
|
||||||
val url: String,
|
val url: String,
|
||||||
val title: Spanned,
|
val title: String,
|
||||||
val description: Spanned,
|
val description: String,
|
||||||
@SerializedName("author_name") val authorName: String,
|
@SerializedName("author_name") val authorName: String,
|
||||||
val image: String,
|
val image: String,
|
||||||
val type: String,
|
val type: String,
|
||||||
|
@ -31,9 +30,7 @@ data class Card(
|
||||||
val embed_url: String?
|
val embed_url: String?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
override fun hashCode(): Int {
|
override fun hashCode() = url.hashCode()
|
||||||
return url.hashCode()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun equals(other: Any?): Boolean {
|
override fun equals(other: Any?): Boolean {
|
||||||
if (other !is Card) {
|
if (other !is Card) {
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
package com.keylesspalace.tusky.entity
|
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName
|
|
||||||
|
|
||||||
data class IdentityProof(
|
|
||||||
val provider: String,
|
|
||||||
@SerializedName("provider_username") val username: String,
|
|
||||||
@SerializedName("profile_url") val profileUrl: String
|
|
||||||
)
|
|
|
@ -37,7 +37,10 @@ data class Notification(
|
||||||
FOLLOW("follow"),
|
FOLLOW("follow"),
|
||||||
FOLLOW_REQUEST("follow_request"),
|
FOLLOW_REQUEST("follow_request"),
|
||||||
POLL("poll"),
|
POLL("poll"),
|
||||||
STATUS("status");
|
STATUS("status"),
|
||||||
|
SIGN_UP("admin.sign_up"),
|
||||||
|
UPDATE("update"),
|
||||||
|
;
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
@ -49,7 +52,7 @@ data class Notification(
|
||||||
}
|
}
|
||||||
return UNKNOWN
|
return UNKNOWN
|
||||||
}
|
}
|
||||||
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS)
|
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS, SIGN_UP, UPDATE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
|
|
|
@ -16,9 +16,9 @@
|
||||||
package com.keylesspalace.tusky.entity
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
|
||||||
import android.text.style.URLSpan
|
import android.text.style.URLSpan
|
||||||
import com.google.gson.annotations.SerializedName
|
import com.google.gson.annotations.SerializedName
|
||||||
|
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ data class Status(
|
||||||
@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?,
|
||||||
val content: Spanned,
|
val content: String,
|
||||||
@SerializedName("created_at") val createdAt: Date,
|
@SerializedName("created_at") val createdAt: Date,
|
||||||
val emojis: List<Emoji>,
|
val emojis: List<Emoji>,
|
||||||
@SerializedName("reblogs_count") val reblogsCount: Int,
|
@SerializedName("reblogs_count") val reblogsCount: Int,
|
||||||
|
@ -134,8 +134,9 @@ data class Status(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getEditableText(): String {
|
private fun getEditableText(): String {
|
||||||
val builder = SpannableStringBuilder(content)
|
val contentSpanned = content.parseAsMastodonHtml()
|
||||||
for (span in content.getSpans(0, content.length, URLSpan::class.java)) {
|
val builder = SpannableStringBuilder(content.parseAsMastodonHtml())
|
||||||
|
for (span in contentSpanned.getSpans(0, content.length, URLSpan::class.java)) {
|
||||||
val url = span.url
|
val url = span.url
|
||||||
for ((_, url1, username) in mentions) {
|
for ((_, url1, username) in mentions) {
|
||||||
if (url == url1) {
|
if (url == url1) {
|
||||||
|
@ -149,71 +150,6 @@ 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,
|
||||||
|
|
|
@ -15,6 +15,10 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.fragment;
|
package com.keylesspalace.tusky.fragment;
|
||||||
|
|
||||||
|
import static com.keylesspalace.tusky.util.StringUtils.isLessThan;
|
||||||
|
import static autodispose2.AutoDispose.autoDisposable;
|
||||||
|
import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
@ -111,10 +115,6 @@ import kotlin.Unit;
|
||||||
import kotlin.collections.CollectionsKt;
|
import kotlin.collections.CollectionsKt;
|
||||||
import kotlin.jvm.functions.Function1;
|
import kotlin.jvm.functions.Function1;
|
||||||
|
|
||||||
import static autodispose2.AutoDispose.autoDisposable;
|
|
||||||
import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from;
|
|
||||||
import static com.keylesspalace.tusky.util.StringUtils.isLessThan;
|
|
||||||
|
|
||||||
public class NotificationsFragment extends SFragment implements
|
public class NotificationsFragment extends SFragment implements
|
||||||
SwipeRefreshLayout.OnRefreshListener,
|
SwipeRefreshLayout.OnRefreshListener,
|
||||||
StatusActionListener,
|
StatusActionListener,
|
||||||
|
@ -707,6 +707,10 @@ public class NotificationsFragment extends SFragment implements
|
||||||
return getString(R.string.notification_poll_name);
|
return getString(R.string.notification_poll_name);
|
||||||
case STATUS:
|
case STATUS:
|
||||||
return getString(R.string.notification_subscription_name);
|
return getString(R.string.notification_subscription_name);
|
||||||
|
case SIGN_UP:
|
||||||
|
return getString(R.string.notification_sign_up_name);
|
||||||
|
case UPDATE:
|
||||||
|
return getString(R.string.notification_update_name);
|
||||||
default:
|
default:
|
||||||
return "Unknown";
|
return "Unknown";
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.fragment;
|
package com.keylesspalace.tusky.fragment;
|
||||||
|
|
||||||
|
import static com.keylesspalace.tusky.util.StatusParsingHelper.parseAsMastodonHtml;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.app.DownloadManager;
|
import android.app.DownloadManager;
|
||||||
import android.content.ClipData;
|
import android.content.ClipData;
|
||||||
|
@ -56,6 +58,7 @@ import com.keylesspalace.tusky.entity.Status;
|
||||||
import com.keylesspalace.tusky.network.MastodonApi;
|
import com.keylesspalace.tusky.network.MastodonApi;
|
||||||
import com.keylesspalace.tusky.network.TimelineCases;
|
import com.keylesspalace.tusky.network.TimelineCases;
|
||||||
import com.keylesspalace.tusky.util.LinkHelper;
|
import com.keylesspalace.tusky.util.LinkHelper;
|
||||||
|
import com.keylesspalace.tusky.util.StatusParsingHelper;
|
||||||
import com.keylesspalace.tusky.view.MuteAccountDialog;
|
import com.keylesspalace.tusky.view.MuteAccountDialog;
|
||||||
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
|
||||||
|
|
||||||
|
@ -150,7 +153,7 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
composeOptions.setContentWarning(contentWarning);
|
composeOptions.setContentWarning(contentWarning);
|
||||||
composeOptions.setMentionedUsernames(mentionedUsernames);
|
composeOptions.setMentionedUsernames(mentionedUsernames);
|
||||||
composeOptions.setReplyingStatusAuthor(actionableStatus.getAccount().getLocalUsername());
|
composeOptions.setReplyingStatusAuthor(actionableStatus.getAccount().getLocalUsername());
|
||||||
composeOptions.setReplyingStatusContent(actionableStatus.getContent().toString());
|
composeOptions.setReplyingStatusContent(parseAsMastodonHtml(actionableStatus.getContent()).toString());
|
||||||
|
|
||||||
Intent intent = ComposeActivity.startIntent(getContext(), composeOptions);
|
Intent intent = ComposeActivity.startIntent(getContext(), composeOptions);
|
||||||
getActivity().startActivity(intent);
|
getActivity().startActivity(intent);
|
||||||
|
@ -226,7 +229,7 @@ public abstract class SFragment extends Fragment implements Injectable {
|
||||||
|
|
||||||
String stringToShare = statusToShare.getAccount().getUsername() +
|
String stringToShare = statusToShare.getAccount().getUsername() +
|
||||||
" - " +
|
" - " +
|
||||||
statusToShare.getContent().toString();
|
StatusParsingHelper.parseAsMastodonHtml(statusToShare.getContent()).toString();
|
||||||
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");
|
||||||
|
|
|
@ -38,7 +38,7 @@ public interface StatusActionListener extends LinkListener {
|
||||||
void onOpenReblog(int position);
|
void onOpenReblog(int position);
|
||||||
void onExpandedChange(boolean expanded, int position);
|
void onExpandedChange(boolean expanded, int position);
|
||||||
void onContentHiddenChange(boolean isShowing, int position);
|
void onContentHiddenChange(boolean isShowing, int position);
|
||||||
void onLoadMore(int position);
|
void onLoadMore(int position);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the status {@link android.widget.ToggleButton} responsible for collapsing long
|
* Called when the status {@link android.widget.ToggleButton} responsible for collapsing long
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
/* Copyright 2020 Tusky Contributors
|
|
||||||
*
|
|
||||||
* This file is a part of Tusky.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
||||||
* Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky.json
|
|
||||||
|
|
||||||
import android.text.Spanned
|
|
||||||
import android.text.SpannedString
|
|
||||||
import androidx.core.text.HtmlCompat
|
|
||||||
import androidx.core.text.parseAsHtml
|
|
||||||
import androidx.core.text.toHtml
|
|
||||||
import com.google.gson.JsonDeserializationContext
|
|
||||||
import com.google.gson.JsonDeserializer
|
|
||||||
import com.google.gson.JsonElement
|
|
||||||
import com.google.gson.JsonParseException
|
|
||||||
import com.google.gson.JsonPrimitive
|
|
||||||
import com.google.gson.JsonSerializationContext
|
|
||||||
import com.google.gson.JsonSerializer
|
|
||||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
|
||||||
import java.lang.reflect.Type
|
|
||||||
|
|
||||||
class SpannedTypeAdapter : JsonDeserializer<Spanned>, JsonSerializer<Spanned?> {
|
|
||||||
@Throws(JsonParseException::class)
|
|
||||||
override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Spanned {
|
|
||||||
return json.asString
|
|
||||||
/* Mastodon uses 'white-space: pre-wrap;' so spaces are displayed as returned by the Api.
|
|
||||||
* We can't use CSS so we replace spaces with non-breaking-spaces to emulate the behavior.
|
|
||||||
*/
|
|
||||||
?.replace("<br> ", "<br> ")
|
|
||||||
?.replace("<br /> ", "<br /> ")
|
|
||||||
?.replace("<br/> ", "<br/> ")
|
|
||||||
?.replace(" ", " ")
|
|
||||||
?.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 {
|
|
||||||
return JsonPrimitive(src!!.toHtml(HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -24,7 +24,6 @@ import com.keylesspalace.tusky.entity.Conversation
|
||||||
import com.keylesspalace.tusky.entity.DeletedStatus
|
import com.keylesspalace.tusky.entity.DeletedStatus
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
import com.keylesspalace.tusky.entity.Emoji
|
||||||
import com.keylesspalace.tusky.entity.Filter
|
import com.keylesspalace.tusky.entity.Filter
|
||||||
import com.keylesspalace.tusky.entity.IdentityProof
|
|
||||||
import com.keylesspalace.tusky.entity.Instance
|
import com.keylesspalace.tusky.entity.Instance
|
||||||
import com.keylesspalace.tusky.entity.Marker
|
import com.keylesspalace.tusky.entity.Marker
|
||||||
import com.keylesspalace.tusky.entity.MastoList
|
import com.keylesspalace.tusky.entity.MastoList
|
||||||
|
@ -77,10 +76,10 @@ interface MastodonApi {
|
||||||
fun getLists(): Single<List<MastoList>>
|
fun getLists(): Single<List<MastoList>>
|
||||||
|
|
||||||
@GET("/api/v1/custom_emojis")
|
@GET("/api/v1/custom_emojis")
|
||||||
fun getCustomEmojis(): Single<List<Emoji>>
|
suspend fun getCustomEmojis(): Result<List<Emoji>>
|
||||||
|
|
||||||
@GET("api/v1/instance")
|
@GET("api/v1/instance")
|
||||||
fun getInstance(): Single<Instance>
|
suspend fun getInstance(): Result<Instance>
|
||||||
|
|
||||||
@GET("api/v1/filters")
|
@GET("api/v1/filters")
|
||||||
fun getFilters(): Single<List<Filter>>
|
fun getFilters(): Single<List<Filter>>
|
||||||
|
@ -143,27 +142,25 @@ interface MastodonApi {
|
||||||
@POST("api/v1/notifications/clear")
|
@POST("api/v1/notifications/clear")
|
||||||
fun clearNotifications(): Single<ResponseBody>
|
fun clearNotifications(): Single<ResponseBody>
|
||||||
|
|
||||||
@Multipart
|
|
||||||
@POST("api/v2/media")
|
|
||||||
fun uploadMedia(
|
|
||||||
@Part file: MultipartBody.Part,
|
|
||||||
@Part description: MultipartBody.Part? = null
|
|
||||||
): Single<MediaUploadResult>
|
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@PUT("api/v1/media/{mediaId}")
|
@PUT("api/v1/media/{mediaId}")
|
||||||
fun updateMedia(
|
suspend fun updateMedia(
|
||||||
@Path("mediaId") mediaId: String,
|
@Path("mediaId") mediaId: String,
|
||||||
@Field("description") description: String
|
@Field("description") description: String
|
||||||
): Single<Attachment>
|
): Result<Attachment>
|
||||||
|
|
||||||
|
@GET("api/v1/media/{mediaId}")
|
||||||
|
suspend fun getMedia(
|
||||||
|
@Path("mediaId") mediaId: String
|
||||||
|
): Response<MediaUploadResult>
|
||||||
|
|
||||||
@POST("api/v1/statuses")
|
@POST("api/v1/statuses")
|
||||||
fun createStatus(
|
suspend fun createStatus(
|
||||||
@Header("Authorization") auth: String,
|
@Header("Authorization") auth: String,
|
||||||
@Header(DOMAIN_HEADER) domain: String,
|
@Header(DOMAIN_HEADER) domain: String,
|
||||||
@Header("Idempotency-Key") idempotencyKey: String,
|
@Header("Idempotency-Key") idempotencyKey: String,
|
||||||
@Body status: NewStatus
|
@Body status: NewStatus
|
||||||
): Call<Status>
|
): Result<Status>
|
||||||
|
|
||||||
@GET("api/v1/statuses/{id}")
|
@GET("api/v1/statuses/{id}")
|
||||||
fun status(
|
fun status(
|
||||||
|
@ -249,12 +246,12 @@ interface MastodonApi {
|
||||||
): Single<List<ScheduledStatus>>
|
): Single<List<ScheduledStatus>>
|
||||||
|
|
||||||
@DELETE("api/v1/scheduled_statuses/{id}")
|
@DELETE("api/v1/scheduled_statuses/{id}")
|
||||||
fun deleteScheduledStatus(
|
suspend fun deleteScheduledStatus(
|
||||||
@Path("id") scheduledStatusId: String
|
@Path("id") scheduledStatusId: String
|
||||||
): Single<ResponseBody>
|
): Result<ResponseBody>
|
||||||
|
|
||||||
@GET("api/v1/accounts/verify_credentials")
|
@GET("api/v1/accounts/verify_credentials")
|
||||||
fun accountVerifyCredentials(): Single<Account>
|
suspend fun accountVerifyCredentials(): Result<Account>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@PATCH("api/v1/accounts/update_credentials")
|
@PATCH("api/v1/accounts/update_credentials")
|
||||||
|
@ -265,7 +262,7 @@ interface MastodonApi {
|
||||||
|
|
||||||
@Multipart
|
@Multipart
|
||||||
@PATCH("api/v1/accounts/update_credentials")
|
@PATCH("api/v1/accounts/update_credentials")
|
||||||
fun accountUpdateCredentials(
|
suspend fun accountUpdateCredentials(
|
||||||
@Part(value = "display_name") displayName: RequestBody?,
|
@Part(value = "display_name") displayName: RequestBody?,
|
||||||
@Part(value = "note") note: RequestBody?,
|
@Part(value = "note") note: RequestBody?,
|
||||||
@Part(value = "locked") locked: RequestBody?,
|
@Part(value = "locked") locked: RequestBody?,
|
||||||
|
@ -279,7 +276,7 @@ interface MastodonApi {
|
||||||
@Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?,
|
@Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?,
|
||||||
@Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?,
|
@Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?,
|
||||||
@Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody?
|
@Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody?
|
||||||
): Call<Account>
|
): Result<Account>
|
||||||
|
|
||||||
@GET("api/v1/accounts/search")
|
@GET("api/v1/accounts/search")
|
||||||
fun searchAccounts(
|
fun searchAccounts(
|
||||||
|
@ -367,11 +364,6 @@ interface MastodonApi {
|
||||||
@Query("id[]") accountIds: List<String>
|
@Query("id[]") accountIds: List<String>
|
||||||
): Single<List<Relationship>>
|
): Single<List<Relationship>>
|
||||||
|
|
||||||
@GET("api/v1/accounts/{id}/identity_proofs")
|
|
||||||
fun identityProofs(
|
|
||||||
@Path("id") accountId: String
|
|
||||||
): Single<List<IdentityProof>>
|
|
||||||
|
|
||||||
@POST("api/v1/pleroma/accounts/{id}/subscribe")
|
@POST("api/v1/pleroma/accounts/{id}/subscribe")
|
||||||
fun subscribeAccount(
|
fun subscribeAccount(
|
||||||
@Path("id") accountId: String
|
@Path("id") accountId: String
|
||||||
|
@ -447,7 +439,7 @@ interface MastodonApi {
|
||||||
@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
|
||||||
): AppCredentials
|
): Result<AppCredentials>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("oauth/token")
|
@POST("oauth/token")
|
||||||
|
@ -458,7 +450,7 @@ interface MastodonApi {
|
||||||
@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
|
||||||
): AccessToken
|
): Result<AccessToken>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("api/v1/lists")
|
@POST("api/v1/lists")
|
||||||
|
@ -544,26 +536,26 @@ interface MastodonApi {
|
||||||
): Single<Poll>
|
): Single<Poll>
|
||||||
|
|
||||||
@GET("api/v1/announcements")
|
@GET("api/v1/announcements")
|
||||||
fun listAnnouncements(
|
suspend fun listAnnouncements(
|
||||||
@Query("with_dismissed") withDismissed: Boolean = true
|
@Query("with_dismissed") withDismissed: Boolean = true
|
||||||
): Single<List<Announcement>>
|
): Result<List<Announcement>>
|
||||||
|
|
||||||
@POST("api/v1/announcements/{id}/dismiss")
|
@POST("api/v1/announcements/{id}/dismiss")
|
||||||
fun dismissAnnouncement(
|
suspend fun dismissAnnouncement(
|
||||||
@Path("id") announcementId: String
|
@Path("id") announcementId: String
|
||||||
): Single<ResponseBody>
|
): Result<ResponseBody>
|
||||||
|
|
||||||
@PUT("api/v1/announcements/{id}/reactions/{name}")
|
@PUT("api/v1/announcements/{id}/reactions/{name}")
|
||||||
fun addAnnouncementReaction(
|
suspend fun addAnnouncementReaction(
|
||||||
@Path("id") announcementId: String,
|
@Path("id") announcementId: String,
|
||||||
@Path("name") name: String
|
@Path("name") name: String
|
||||||
): Single<ResponseBody>
|
): Result<ResponseBody>
|
||||||
|
|
||||||
@DELETE("api/v1/announcements/{id}/reactions/{name}")
|
@DELETE("api/v1/announcements/{id}/reactions/{name}")
|
||||||
fun removeAnnouncementReaction(
|
suspend fun removeAnnouncementReaction(
|
||||||
@Path("id") announcementId: String,
|
@Path("id") announcementId: String,
|
||||||
@Path("name") name: String
|
@Path("name") name: String
|
||||||
): Single<ResponseBody>
|
): Result<ResponseBody>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("api/v1/reports")
|
@POST("api/v1/reports")
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.keylesspalace.tusky.network
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.entity.MediaUploadResult
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import retrofit2.http.Multipart
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Part
|
||||||
|
|
||||||
|
/** endpoints defined in this interface will be called with a higher timeout than usual
|
||||||
|
* which is necessary for media uploads to succeed on some servers
|
||||||
|
*/
|
||||||
|
interface MediaUploadApi {
|
||||||
|
@Multipart
|
||||||
|
@POST("api/v2/media")
|
||||||
|
suspend fun uploadMedia(
|
||||||
|
@Part file: MultipartBody.Part,
|
||||||
|
@Part description: MultipartBody.Part? = null
|
||||||
|
): Result<MediaUploadResult>
|
||||||
|
}
|
|
@ -100,7 +100,8 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
||||||
accountId = account.id,
|
accountId = account.id,
|
||||||
draftId = -1,
|
draftId = -1,
|
||||||
idempotencyKey = randomAlphanumericString(16),
|
idempotencyKey = randomAlphanumericString(16),
|
||||||
retries = 0
|
retries = 0,
|
||||||
|
mediaProcessed = mutableListOf()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import android.util.Log
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.ServiceCompat
|
import androidx.core.app.ServiceCompat
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
@ -29,13 +30,12 @@ import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import dagger.android.AndroidInjection
|
import dagger.android.AndroidInjection
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.delay
|
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.HttpException
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
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
|
||||||
|
@ -55,7 +55,7 @@ class SendStatusService : Service(), Injectable {
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.Main + supervisorJob)
|
private val serviceScope = CoroutineScope(Dispatchers.Main + supervisorJob)
|
||||||
|
|
||||||
private val statusesToSend = ConcurrentHashMap<Int, StatusToSend>()
|
private val statusesToSend = ConcurrentHashMap<Int, StatusToSend>()
|
||||||
private val sendCalls = ConcurrentHashMap<Int, Call<Status>>()
|
private val sendJobs = ConcurrentHashMap<Int, Job>()
|
||||||
|
|
||||||
private val notificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
|
private val notificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
|
||||||
|
|
||||||
|
@ -64,12 +64,9 @@ class SendStatusService : Service(), Injectable {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent): IBinder? {
|
override fun onBind(intent: Intent): IBinder? = null
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||||
|
|
||||||
if (intent.hasExtra(KEY_STATUS)) {
|
if (intent.hasExtra(KEY_STATUS)) {
|
||||||
val statusToSend = intent.getParcelableExtra<StatusToSend>(KEY_STATUS)
|
val statusToSend = intent.getParcelableExtra<StatusToSend>(KEY_STATUS)
|
||||||
?: throw IllegalStateException("SendStatusService started without $KEY_STATUS extra")
|
?: throw IllegalStateException("SendStatusService started without $KEY_STATUS extra")
|
||||||
|
@ -129,82 +126,94 @@ class SendStatusService : Service(), Injectable {
|
||||||
|
|
||||||
statusToSend.retries++
|
statusToSend.retries++
|
||||||
|
|
||||||
val newStatus = NewStatus(
|
sendJobs[statusId] = serviceScope.launch {
|
||||||
statusToSend.text,
|
try {
|
||||||
statusToSend.warningText,
|
var mediaCheckRetries = 0
|
||||||
statusToSend.inReplyToId,
|
while (statusToSend.mediaProcessed.any { !it }) {
|
||||||
statusToSend.visibility,
|
delay(1000L * mediaCheckRetries)
|
||||||
statusToSend.sensitive,
|
statusToSend.mediaProcessed.forEachIndexed { index, processed ->
|
||||||
statusToSend.mediaIds,
|
if (!processed) {
|
||||||
statusToSend.scheduledAt,
|
// Mastodon returns 206 if the media was not yet processed
|
||||||
statusToSend.poll
|
statusToSend.mediaProcessed[index] = mastodonApi.getMedia(statusToSend.mediaIds[index]).code() == 200
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
mediaCheckRetries ++
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w(TAG, "failed getting media status", e)
|
||||||
|
retrySending(statusId)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
|
||||||
val sendCall = mastodonApi.createStatus(
|
val newStatus = NewStatus(
|
||||||
"Bearer " + account.accessToken,
|
statusToSend.text,
|
||||||
account.domain,
|
statusToSend.warningText,
|
||||||
statusToSend.idempotencyKey,
|
statusToSend.inReplyToId,
|
||||||
newStatus
|
statusToSend.visibility,
|
||||||
)
|
statusToSend.sensitive,
|
||||||
|
statusToSend.mediaIds,
|
||||||
|
statusToSend.scheduledAt,
|
||||||
|
statusToSend.poll
|
||||||
|
)
|
||||||
|
|
||||||
sendCalls[statusId] = sendCall
|
mastodonApi.createStatus(
|
||||||
|
"Bearer " + account.accessToken,
|
||||||
|
account.domain,
|
||||||
|
statusToSend.idempotencyKey,
|
||||||
|
newStatus
|
||||||
|
).fold({ sentStatus ->
|
||||||
|
statusesToSend.remove(statusId)
|
||||||
|
// If the status was loaded from a draft, delete the draft and associated media files.
|
||||||
|
if (statusToSend.draftId != 0) {
|
||||||
|
draftHelper.deleteDraftAndAttachments(statusToSend.draftId)
|
||||||
|
}
|
||||||
|
|
||||||
val callback = object : Callback<Status> {
|
val scheduled = !statusToSend.scheduledAt.isNullOrEmpty()
|
||||||
override fun onResponse(call: Call<Status>, response: Response<Status>) {
|
|
||||||
serviceScope.launch {
|
|
||||||
|
|
||||||
val scheduled = !statusToSend.scheduledAt.isNullOrEmpty()
|
if (scheduled) {
|
||||||
|
eventHub.dispatch(StatusScheduledEvent(sentStatus))
|
||||||
|
} else {
|
||||||
|
eventHub.dispatch(StatusComposedEvent(sentStatus))
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationManager.cancel(statusId)
|
||||||
|
}, { throwable ->
|
||||||
|
Log.w(TAG, "failed sending status", throwable)
|
||||||
|
if (throwable is HttpException) {
|
||||||
|
// the server refused to accept the status, save status & show error message
|
||||||
statusesToSend.remove(statusId)
|
statusesToSend.remove(statusId)
|
||||||
|
saveStatusToDrafts(statusToSend)
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
val builder = NotificationCompat.Builder(this@SendStatusService, CHANNEL_ID)
|
||||||
// If the status was loaded from a draft, delete the draft and associated media files.
|
.setSmallIcon(R.drawable.ic_notify)
|
||||||
if (statusToSend.draftId != 0) {
|
.setContentTitle(getString(R.string.send_post_notification_error_title))
|
||||||
draftHelper.deleteDraftAndAttachments(statusToSend.draftId)
|
.setContentText(getString(R.string.send_post_notification_saved_content))
|
||||||
}
|
.setColor(
|
||||||
|
ContextCompat.getColor(
|
||||||
if (scheduled) {
|
this@SendStatusService,
|
||||||
response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch)
|
R.color.notification_color
|
||||||
} else {
|
|
||||||
response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationManager.cancel(statusId)
|
|
||||||
} else {
|
|
||||||
// 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.cancel(statusId)
|
||||||
notificationManager.notify(errorNotificationId--, builder.build())
|
notificationManager.notify(errorNotificationId--, builder.build())
|
||||||
}
|
} else {
|
||||||
stopSelfWhenDone()
|
// a network problem occurred, let's retry sending the status
|
||||||
|
retrySending(statusId)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
stopSelfWhenDone()
|
||||||
override fun onFailure(call: Call<Status>, t: Throwable) {
|
|
||||||
serviceScope.launch {
|
|
||||||
var backoff = TimeUnit.SECONDS.toMillis(statusToSend.retries.toLong())
|
|
||||||
if (backoff > MAX_RETRY_INTERVAL) {
|
|
||||||
backoff = MAX_RETRY_INTERVAL
|
|
||||||
}
|
|
||||||
|
|
||||||
delay(backoff)
|
|
||||||
sendStatus(statusId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sendCall.enqueue(callback)
|
private suspend fun retrySending(statusId: Int) {
|
||||||
|
// when statusToSend == null, sending has been canceled
|
||||||
|
val statusToSend = statusesToSend[statusId] ?: return
|
||||||
|
|
||||||
|
val backoff = TimeUnit.SECONDS.toMillis(statusToSend.retries.toLong()).coerceAtMost(MAX_RETRY_INTERVAL)
|
||||||
|
|
||||||
|
delay(backoff)
|
||||||
|
sendStatus(statusId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopSelfWhenDone() {
|
private fun stopSelfWhenDone() {
|
||||||
|
@ -218,8 +227,8 @@ class SendStatusService : Service(), Injectable {
|
||||||
private fun cancelSending(statusId: Int) = serviceScope.launch {
|
private fun cancelSending(statusId: Int) = serviceScope.launch {
|
||||||
val statusToCancel = statusesToSend.remove(statusId)
|
val statusToCancel = statusesToSend.remove(statusId)
|
||||||
if (statusToCancel != null) {
|
if (statusToCancel != null) {
|
||||||
val sendCall = sendCalls.remove(statusId)
|
val sendJob = sendJobs.remove(statusId)
|
||||||
sendCall?.cancel()
|
sendJob?.cancel()
|
||||||
|
|
||||||
saveStatusToDrafts(statusToCancel)
|
saveStatusToDrafts(statusToCancel)
|
||||||
|
|
||||||
|
@ -263,6 +272,7 @@ class SendStatusService : Service(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
private const val TAG = "SendStatusService"
|
||||||
|
|
||||||
private const val KEY_STATUS = "status"
|
private const val KEY_STATUS = "status"
|
||||||
private const val KEY_CANCEL = "cancel_id"
|
private const val KEY_CANCEL = "cancel_id"
|
||||||
|
@ -319,5 +329,6 @@ data class StatusToSend(
|
||||||
val accountId: Long,
|
val accountId: Long,
|
||||||
val draftId: Int,
|
val draftId: Int,
|
||||||
val idempotencyKey: String,
|
val idempotencyKey: String,
|
||||||
var retries: Int
|
var retries: Int,
|
||||||
|
val mediaProcessed: MutableList<Boolean>
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
|
@ -59,6 +59,8 @@ object PrefKeys {
|
||||||
const val NOTIFICATION_FILTER_FOLLOW_REQUESTS = "notificationFilterFollowRequests"
|
const val NOTIFICATION_FILTER_FOLLOW_REQUESTS = "notificationFilterFollowRequests"
|
||||||
const val NOTIFICATIONS_FILTER_FOLLOWS = "notificationFilterFollows"
|
const val NOTIFICATIONS_FILTER_FOLLOWS = "notificationFilterFollows"
|
||||||
const val NOTIFICATION_FILTER_SUBSCRIPTIONS = "notificationFilterSubscriptions"
|
const val NOTIFICATION_FILTER_SUBSCRIPTIONS = "notificationFilterSubscriptions"
|
||||||
|
const val NOTIFICATION_FILTER_SIGN_UPS = "notificationFilterSignUps"
|
||||||
|
const val NOTIFICATION_FILTER_UPDATES = "notificationFilterUpdates"
|
||||||
|
|
||||||
const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies"
|
const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies"
|
||||||
const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts"
|
const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts"
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
package com.keylesspalace.tusky.settings
|
package com.keylesspalace.tusky.settings
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.activity.result.ActivityResultRegistryOwner
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import androidx.preference.CheckBoxPreference
|
import androidx.preference.CheckBoxPreference
|
||||||
import androidx.preference.EditTextPreference
|
import androidx.preference.EditTextPreference
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
|
@ -10,8 +12,7 @@ import androidx.preference.PreferenceCategory
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import androidx.preference.SwitchPreference
|
import androidx.preference.SwitchPreference
|
||||||
import com.keylesspalace.tusky.components.preference.EmojiPreference
|
import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
|
|
||||||
class PreferenceParent(
|
class PreferenceParent(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
|
@ -32,8 +33,9 @@ inline fun PreferenceParent.listPreference(builder: ListPreference.() -> Unit):
|
||||||
return pref
|
return pref
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun PreferenceParent.emojiPreference(okHttpClient: OkHttpClient, builder: EmojiPreference.() -> Unit): EmojiPreference {
|
inline fun <A> PreferenceParent.emojiPreference(activity: A, builder: EmojiPickerPreference.() -> Unit): EmojiPickerPreference
|
||||||
val pref = EmojiPreference(context, okHttpClient)
|
where A : Context, A : ActivityResultRegistryOwner, A : LifecycleOwner {
|
||||||
|
val pref = EmojiPickerPreference.get(activity)
|
||||||
builder(pref)
|
builder(pref)
|
||||||
addPref(pref)
|
addPref(pref)
|
||||||
return pref
|
return pref
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
/* Copyright 2022 Tusky Contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.util
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
class AbsoluteTimeFormatter @JvmOverloads constructor(private val tz: TimeZone = TimeZone.getDefault()) {
|
||||||
|
private val sameDaySdf = SimpleDateFormat("HH:mm", Locale.getDefault()).apply { this.timeZone = tz }
|
||||||
|
private val sameYearSdf = SimpleDateFormat("MM-dd HH:mm", Locale.getDefault()).apply { this.timeZone = tz }
|
||||||
|
private val otherYearSdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).apply { this.timeZone = tz }
|
||||||
|
private val otherYearCompleteSdf = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).apply { this.timeZone = tz }
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun format(time: Date?, shortFormat: Boolean = true, now: Date = Date()): String {
|
||||||
|
return when {
|
||||||
|
time == null -> "??"
|
||||||
|
isSameDate(time, now, tz) -> sameDaySdf.format(time)
|
||||||
|
isSameYear(time, now, tz) -> sameYearSdf.format(time)
|
||||||
|
shortFormat -> otherYearSdf.format(time)
|
||||||
|
else -> otherYearCompleteSdf.format(time)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private fun isSameDate(dateOne: Date, dateTwo: Date, tz: TimeZone): Boolean {
|
||||||
|
val calendarOne = Calendar.getInstance(tz).apply { time = dateOne }
|
||||||
|
val calendarTwo = Calendar.getInstance(tz).apply { time = dateTwo }
|
||||||
|
|
||||||
|
return calendarOne.get(Calendar.YEAR) == calendarTwo.get(Calendar.YEAR) &&
|
||||||
|
calendarOne.get(Calendar.MONTH) == calendarTwo.get(Calendar.MONTH) &&
|
||||||
|
calendarOne.get(Calendar.DAY_OF_MONTH) == calendarTwo.get(Calendar.DAY_OF_MONTH)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isSameYear(dateOne: Date, dateTwo: Date, timeZone1: TimeZone): Boolean {
|
||||||
|
val calendarOne = Calendar.getInstance(timeZone1).apply { time = dateOne }
|
||||||
|
val calendarTwo = Calendar.getInstance(timeZone1).apply { time = dateTwo }
|
||||||
|
|
||||||
|
return calendarOne.get(Calendar.YEAR) == calendarTwo.get(Calendar.YEAR)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,364 +0,0 @@
|
||||||
package com.keylesspalace.tusky.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import android.util.Pair
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import com.keylesspalace.tusky.R
|
|
||||||
import de.c1710.filemojicompat.FileEmojiCompatConfig
|
|
||||||
import io.reactivex.rxjava3.core.Observable
|
|
||||||
import io.reactivex.rxjava3.core.ObservableEmitter
|
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
||||||
import okhttp3.OkHttpClient
|
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.ResponseBody
|
|
||||||
import okhttp3.internal.toLongOrDefault
|
|
||||||
import okio.Source
|
|
||||||
import okio.buffer
|
|
||||||
import okio.sink
|
|
||||||
import java.io.EOFException
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FilenameFilter
|
|
||||||
import java.io.IOException
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This class bundles information about an emoji font as well as many convenient actions.
|
|
||||||
*/
|
|
||||||
class EmojiCompatFont(
|
|
||||||
val name: String,
|
|
||||||
private val display: String,
|
|
||||||
@StringRes val caption: Int,
|
|
||||||
@DrawableRes val img: Int,
|
|
||||||
val url: String,
|
|
||||||
// The version is stored as a String in the x.xx.xx format (to be able to compare versions)
|
|
||||||
val version: String
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val versionCode = getVersionCode(version)
|
|
||||||
|
|
||||||
// A list of all available font files and whether they are older than the current version or not
|
|
||||||
// They are ordered by their version codes in ascending order
|
|
||||||
private var existingFontFileCache: List<Pair<File, List<Int>>>? = null
|
|
||||||
|
|
||||||
val id: Int
|
|
||||||
get() = FONTS.indexOf(this)
|
|
||||||
|
|
||||||
fun getDisplay(context: Context): String {
|
|
||||||
return if (this !== SYSTEM_DEFAULT) display else context.getString(R.string.system_default)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method will return the actual font file (regardless of its existence) for
|
|
||||||
* the current version (not necessarily the latest!).
|
|
||||||
*
|
|
||||||
* @return The font (TTF) file or null if called on SYSTEM_FONT
|
|
||||||
*/
|
|
||||||
private fun getFontFile(context: Context): File? {
|
|
||||||
return if (this !== SYSTEM_DEFAULT) {
|
|
||||||
val directory = File(context.getExternalFilesDir(null), DIRECTORY)
|
|
||||||
File(directory, "$name$version.ttf")
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getConfig(context: Context): FileEmojiCompatConfig {
|
|
||||||
return FileEmojiCompatConfig(context, getLatestFontFile(context))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isDownloaded(context: Context): Boolean {
|
|
||||||
return this === SYSTEM_DEFAULT || getFontFile(context)?.exists() == true || fontFileExists(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether there is already a font version that satisfies the current version, i.e. it
|
|
||||||
* has a higher or equal version code.
|
|
||||||
*
|
|
||||||
* @param context The Context
|
|
||||||
* @return Whether there is a font file with a higher or equal version code to the current
|
|
||||||
*/
|
|
||||||
private fun fontFileExists(context: Context): Boolean {
|
|
||||||
val existingFontFiles = getExistingFontFiles(context)
|
|
||||||
return if (existingFontFiles.isNotEmpty()) {
|
|
||||||
compareVersions(existingFontFiles.last().second, versionCode) >= 0
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes any older version of a font
|
|
||||||
*
|
|
||||||
* @param context The current Context
|
|
||||||
*/
|
|
||||||
private fun deleteOldVersions(context: Context) {
|
|
||||||
val existingFontFiles = getExistingFontFiles(context)
|
|
||||||
Log.d(TAG, "deleting old versions...")
|
|
||||||
Log.d(TAG, String.format("deleteOldVersions: Found %d other font files", existingFontFiles.size))
|
|
||||||
for (fileExists in existingFontFiles) {
|
|
||||||
if (compareVersions(fileExists.second, versionCode) < 0) {
|
|
||||||
val file = fileExists.first
|
|
||||||
// Uses side effects!
|
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
String.format(
|
|
||||||
"Deleted %s successfully: %s", file.absolutePath,
|
|
||||||
file.delete()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads all font files that are inside the files directory into an ArrayList with the information
|
|
||||||
* on whether they are older than the currently available version or not.
|
|
||||||
*
|
|
||||||
* @param context The Context
|
|
||||||
*/
|
|
||||||
private fun getExistingFontFiles(context: Context): List<Pair<File, List<Int>>> {
|
|
||||||
// Only load it once
|
|
||||||
existingFontFileCache?.let {
|
|
||||||
return it
|
|
||||||
}
|
|
||||||
// If we call this on the system default font, just return nothing...
|
|
||||||
if (this === SYSTEM_DEFAULT) {
|
|
||||||
existingFontFileCache = emptyList()
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val directory = File(context.getExternalFilesDir(null), DIRECTORY)
|
|
||||||
// It will search for old versions using a regex that matches the font's name plus
|
|
||||||
// (if present) a version code. No version code will be regarded as version 0.
|
|
||||||
val fontRegex = "$name(\\d+(\\.\\d+)*)?\\.ttf".toPattern()
|
|
||||||
val ttfFilter = FilenameFilter { _, name: String -> name.endsWith(".ttf") }
|
|
||||||
val foundFontFiles = directory.listFiles(ttfFilter).orEmpty()
|
|
||||||
Log.d(
|
|
||||||
TAG,
|
|
||||||
String.format(
|
|
||||||
"loadExistingFontFiles: %d other font files found",
|
|
||||||
foundFontFiles.size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return foundFontFiles.map { file ->
|
|
||||||
val matcher = fontRegex.matcher(file.name)
|
|
||||||
val versionCode = if (matcher.matches()) {
|
|
||||||
val version = matcher.group(1)
|
|
||||||
getVersionCode(version)
|
|
||||||
} else {
|
|
||||||
listOf(0)
|
|
||||||
}
|
|
||||||
Pair(file, versionCode)
|
|
||||||
}.sortedWith { a, b ->
|
|
||||||
compareVersions(a.second, b.second)
|
|
||||||
}.also {
|
|
||||||
existingFontFileCache = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the current or latest version of this font file (if there is any)
|
|
||||||
*
|
|
||||||
* @param context The Context
|
|
||||||
* @return The file for this font with the current or (if not existent) highest version code or null if there is no file for this font.
|
|
||||||
*/
|
|
||||||
private fun getLatestFontFile(context: Context): File? {
|
|
||||||
val current = getFontFile(context)
|
|
||||||
if (current != null && current.exists()) return current
|
|
||||||
val existingFontFiles = getExistingFontFiles(context)
|
|
||||||
return existingFontFiles.firstOrNull()?.first
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getVersionCode(version: String?): List<Int> {
|
|
||||||
if (version == null) return listOf(0)
|
|
||||||
return version.split(".").map {
|
|
||||||
it.toIntOrNull() ?: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun downloadFontFile(
|
|
||||||
context: Context,
|
|
||||||
okHttpClient: OkHttpClient
|
|
||||||
): Observable<Float> {
|
|
||||||
return Observable.create { emitter: ObservableEmitter<Float> ->
|
|
||||||
// It is possible (and very likely) that the file does not exist yet
|
|
||||||
val downloadFile = getFontFile(context)!!
|
|
||||||
if (!downloadFile.exists()) {
|
|
||||||
downloadFile.parentFile?.mkdirs()
|
|
||||||
downloadFile.createNewFile()
|
|
||||||
}
|
|
||||||
val request = Request.Builder().url(url)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val sink = downloadFile.sink().buffer()
|
|
||||||
var source: Source? = null
|
|
||||||
try {
|
|
||||||
// Download!
|
|
||||||
val response = okHttpClient.newCall(request).execute()
|
|
||||||
|
|
||||||
val responseBody = response.body
|
|
||||||
if (response.isSuccessful && responseBody != null) {
|
|
||||||
val size = response.length()
|
|
||||||
var progress = 0f
|
|
||||||
source = responseBody.source()
|
|
||||||
try {
|
|
||||||
while (!emitter.isDisposed) {
|
|
||||||
sink.write(source, CHUNK_SIZE)
|
|
||||||
progress += CHUNK_SIZE.toFloat()
|
|
||||||
if (size > 0) {
|
|
||||||
emitter.onNext(progress / size)
|
|
||||||
} else {
|
|
||||||
emitter.onNext(-1f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (ex: EOFException) {
|
|
||||||
/*
|
|
||||||
This means we've finished downloading the file since sink.write
|
|
||||||
will throw an EOFException when the file to be read is empty.
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.e(TAG, "Downloading $url failed. Status code: ${response.code}")
|
|
||||||
emitter.tryOnError(Exception())
|
|
||||||
}
|
|
||||||
} catch (ex: IOException) {
|
|
||||||
Log.e(TAG, "Downloading $url failed.", ex)
|
|
||||||
downloadFile.deleteIfExists()
|
|
||||||
emitter.tryOnError(ex)
|
|
||||||
} finally {
|
|
||||||
source?.close()
|
|
||||||
sink.close()
|
|
||||||
if (emitter.isDisposed) {
|
|
||||||
downloadFile.deleteIfExists()
|
|
||||||
} else {
|
|
||||||
deleteOldVersions(context)
|
|
||||||
emitter.onComplete()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the downloaded file, if it exists. Should be called when a download gets cancelled.
|
|
||||||
*/
|
|
||||||
fun deleteDownloadedFile(context: Context) {
|
|
||||||
getFontFile(context)?.deleteIfExists()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return display
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "EmojiCompatFont"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This String represents the sub-directory the fonts are stored in.
|
|
||||||
*/
|
|
||||||
private const val DIRECTORY = "emoji"
|
|
||||||
|
|
||||||
private const val CHUNK_SIZE = 4096L
|
|
||||||
|
|
||||||
// The system font gets some special behavior...
|
|
||||||
val SYSTEM_DEFAULT = EmojiCompatFont(
|
|
||||||
"system-default",
|
|
||||||
"System Default",
|
|
||||||
R.string.caption_systememoji,
|
|
||||||
R.drawable.ic_emoji_34dp,
|
|
||||||
"",
|
|
||||||
"0"
|
|
||||||
)
|
|
||||||
val BLOBMOJI = EmojiCompatFont(
|
|
||||||
"Blobmoji",
|
|
||||||
"Blobmoji",
|
|
||||||
R.string.caption_blobmoji,
|
|
||||||
R.drawable.ic_blobmoji,
|
|
||||||
"https://tusky.app/hosted/emoji/BlobmojiCompat.ttf",
|
|
||||||
"14.0.1"
|
|
||||||
)
|
|
||||||
val TWEMOJI = EmojiCompatFont(
|
|
||||||
"Twemoji",
|
|
||||||
"Twemoji",
|
|
||||||
R.string.caption_twemoji,
|
|
||||||
R.drawable.ic_twemoji,
|
|
||||||
"https://tusky.app/hosted/emoji/TwemojiCompat.ttf",
|
|
||||||
"14.0.0"
|
|
||||||
)
|
|
||||||
val NOTOEMOJI = EmojiCompatFont(
|
|
||||||
"NotoEmoji",
|
|
||||||
"Noto Emoji",
|
|
||||||
R.string.caption_notoemoji,
|
|
||||||
R.drawable.ic_notoemoji,
|
|
||||||
"https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf",
|
|
||||||
"14.0.0"
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This array stores all available EmojiCompat fonts.
|
|
||||||
* References to them can simply be saved by saving their indices
|
|
||||||
*/
|
|
||||||
val FONTS = listOf(SYSTEM_DEFAULT, BLOBMOJI, TWEMOJI, NOTOEMOJI)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Emoji font associated with this ID
|
|
||||||
*
|
|
||||||
* @param id the ID of this font
|
|
||||||
* @return the corresponding font. Will default to SYSTEM_DEFAULT if not in range.
|
|
||||||
*/
|
|
||||||
fun byId(id: Int): EmojiCompatFont = FONTS.getOrElse(id) { SYSTEM_DEFAULT }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compares two version codes to each other
|
|
||||||
*
|
|
||||||
* @param versionA The first version
|
|
||||||
* @param versionB The second version
|
|
||||||
* @return -1 if versionA < versionB, 1 if versionA > versionB and 0 otherwise
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
|
||||||
fun compareVersions(versionA: List<Int>, versionB: List<Int>): Int {
|
|
||||||
val len = max(versionB.size, versionA.size)
|
|
||||||
for (i in 0 until len) {
|
|
||||||
|
|
||||||
val vA = versionA.getOrElse(i) { 0 }
|
|
||||||
val vB = versionB.getOrElse(i) { 0 }
|
|
||||||
|
|
||||||
// It needs to be decided on the next level
|
|
||||||
if (vA == vB) continue
|
|
||||||
// Okay, is version B newer or version A?
|
|
||||||
return vA.compareTo(vB)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The versions are equal
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method is needed because when transparent compression is used OkHttp reports
|
|
||||||
* [ResponseBody.contentLength] as -1. We try to get the header which server sent
|
|
||||||
* us manually here.
|
|
||||||
*
|
|
||||||
* @see [OkHttp issue 259](https://github.com/square/okhttp/issues/259)
|
|
||||||
*/
|
|
||||||
private fun Response.length(): Long {
|
|
||||||
networkResponse?.let {
|
|
||||||
val header = it.header("Content-Length") ?: return -1
|
|
||||||
return header.toLongOrDefault(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// In case it's a fully cached response
|
|
||||||
return body?.contentLength() ?: -1
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun File.deleteIfExists() {
|
|
||||||
if (exists() && !delete()) {
|
|
||||||
Log.e(TAG, "Could not delete file $this")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
/* Copyright 2022 Tusky Contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
@file:JvmName("StatusParsingHelper")
|
||||||
|
package com.keylesspalace.tusky.util
|
||||||
|
|
||||||
|
import android.text.SpannableStringBuilder
|
||||||
|
import android.text.Spanned
|
||||||
|
import androidx.core.text.parseAsHtml
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parse a String containing html from the Mastodon api to Spanned
|
||||||
|
*/
|
||||||
|
fun String.parseAsMastodonHtml(): Spanned {
|
||||||
|
return this.replace("<br> ", "<br> ")
|
||||||
|
.replace("<br /> ", "<br /> ")
|
||||||
|
.replace("<br/> ", "<br/> ")
|
||||||
|
.replace(" ", " ")
|
||||||
|
.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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun replaceCrashingCharacters(content: Spanned): Spanned {
|
||||||
|
return replaceCrashingCharacters(content as CharSequence) as Spanned
|
||||||
|
}
|
||||||
|
|
||||||
|
fun replaceCrashingCharacters(content: CharSequence): CharSequence? {
|
||||||
|
var replacing = false
|
||||||
|
var builder: SpannableStringBuilder? = null
|
||||||
|
val length = content.length
|
||||||
|
for (index in 0 until length) {
|
||||||
|
val character = content[index]
|
||||||
|
|
||||||
|
// If there are more than one or two, switch to a map
|
||||||
|
if (character == SOFT_HYPHEN) {
|
||||||
|
if (!replacing) {
|
||||||
|
replacing = true
|
||||||
|
builder = SpannableStringBuilder(content, 0, index)
|
||||||
|
}
|
||||||
|
builder!!.append(ASCII_HYPHEN)
|
||||||
|
} else if (replacing) {
|
||||||
|
builder!!.append(character)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return if (replacing) builder else content
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val SOFT_HYPHEN = '\u00ad'
|
||||||
|
private const val ASCII_HYPHEN = '-'
|
|
@ -34,20 +34,16 @@ import com.keylesspalace.tusky.viewdata.PollViewData
|
||||||
import com.keylesspalace.tusky.viewdata.buildDescription
|
import com.keylesspalace.tusky.viewdata.buildDescription
|
||||||
import com.keylesspalace.tusky.viewdata.calculatePercent
|
import com.keylesspalace.tusky.viewdata.calculatePercent
|
||||||
import java.text.NumberFormat
|
import java.text.NumberFormat
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Date
|
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
class StatusViewHelper(private val itemView: View) {
|
class StatusViewHelper(private val itemView: View) {
|
||||||
|
private val absoluteTimeFormatter = AbsoluteTimeFormatter()
|
||||||
|
|
||||||
interface MediaPreviewListener {
|
interface MediaPreviewListener {
|
||||||
fun onViewMedia(v: View?, idx: Int)
|
fun onViewMedia(v: View?, idx: Int)
|
||||||
fun onContentHiddenChange(isShowing: Boolean)
|
fun onContentHiddenChange(isShowing: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val shortSdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault())
|
|
||||||
private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault())
|
|
||||||
|
|
||||||
fun setMediasPreview(
|
fun setMediasPreview(
|
||||||
statusDisplayOptions: StatusDisplayOptions,
|
statusDisplayOptions: StatusDisplayOptions,
|
||||||
attachments: List<Attachment>,
|
attachments: List<Attachment>,
|
||||||
|
@ -295,7 +291,7 @@ class StatusViewHelper(private val itemView: View) {
|
||||||
context.getString(R.string.poll_info_closed)
|
context.getString(R.string.poll_info_closed)
|
||||||
} else {
|
} else {
|
||||||
if (useAbsoluteTime) {
|
if (useAbsoluteTime) {
|
||||||
context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.expiresAt))
|
context.getString(R.string.poll_info_time_absolute, absoluteTimeFormatter.format(poll.expiresAt, false))
|
||||||
} else {
|
} else {
|
||||||
TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp)
|
TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp)
|
||||||
}
|
}
|
||||||
|
@ -330,18 +326,6 @@ class StatusViewHelper(private val itemView: View) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAbsoluteTime(time: Date?): String {
|
|
||||||
return if (time != null) {
|
|
||||||
if (android.text.format.DateUtils.isToday(time.time)) {
|
|
||||||
shortSdf.format(time)
|
|
||||||
} else {
|
|
||||||
longSdf.format(time)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
"??:??:??"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val COLLAPSE_INPUT_FILTER = arrayOf<InputFilter>(SmartLengthInputFilter)
|
val COLLAPSE_INPUT_FILTER = arrayOf<InputFilter>(SmartLengthInputFilter)
|
||||||
val NO_INPUT_FILTER = arrayOfNulls<InputFilter>(0)
|
val NO_INPUT_FILTER = arrayOfNulls<InputFilter>(0)
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
/* Copyright 2019 kyori19
|
|
||||||
*
|
|
||||||
* 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.util;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
public class VersionUtils {
|
|
||||||
|
|
||||||
private int major;
|
|
||||||
private int minor;
|
|
||||||
private int patch;
|
|
||||||
|
|
||||||
public VersionUtils(@NonNull String versionString) {
|
|
||||||
String regex = "([0-9]+)\\.([0-9]+)\\.([0-9]+).*";
|
|
||||||
Pattern pattern = Pattern.compile(regex);
|
|
||||||
Matcher matcher = pattern.matcher(versionString);
|
|
||||||
if (matcher.find()) {
|
|
||||||
major = Integer.parseInt(matcher.group(1));
|
|
||||||
minor = Integer.parseInt(matcher.group(2));
|
|
||||||
patch = Integer.parseInt(matcher.group(3));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean supportsScheduledToots() {
|
|
||||||
return (major == 2) ? ( (minor == 7) ? (patch >= 0) : (minor > 7) ) : (major > 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -27,12 +27,9 @@ fun Status.toViewData(
|
||||||
isExpanded: Boolean,
|
isExpanded: Boolean,
|
||||||
isCollapsed: Boolean
|
isCollapsed: Boolean
|
||||||
): StatusViewData.Concrete {
|
): StatusViewData.Concrete {
|
||||||
val visibleStatus = this.reblog ?: this
|
|
||||||
|
|
||||||
return StatusViewData.Concrete(
|
return StatusViewData.Concrete(
|
||||||
status = this,
|
status = this,
|
||||||
isShowingContent = isShowingContent,
|
isShowingContent = isShowingContent,
|
||||||
isCollapsible = shouldTrimStatus(visibleStatus.content),
|
|
||||||
isCollapsed = isCollapsed,
|
isCollapsed = isCollapsed,
|
||||||
isExpanded = isExpanded,
|
isExpanded = isExpanded,
|
||||||
)
|
)
|
||||||
|
|
|
@ -15,9 +15,11 @@
|
||||||
package com.keylesspalace.tusky.viewdata
|
package com.keylesspalace.tusky.viewdata
|
||||||
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.text.SpannableStringBuilder
|
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.util.parseAsMastodonHtml
|
||||||
|
import com.keylesspalace.tusky.util.replaceCrashingCharacters
|
||||||
|
import com.keylesspalace.tusky.util.shouldTrimStatus
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by charlag on 11/07/2017.
|
* Created by charlag on 11/07/2017.
|
||||||
|
@ -32,13 +34,6 @@ sealed class StatusViewData {
|
||||||
val status: Status,
|
val status: Status,
|
||||||
val isExpanded: Boolean,
|
val isExpanded: Boolean,
|
||||||
val isShowingContent: Boolean,
|
val isShowingContent: Boolean,
|
||||||
/**
|
|
||||||
* Specifies whether the content of this post is allowed to be collapsed or if it should show
|
|
||||||
* all content regardless.
|
|
||||||
*
|
|
||||||
* @return Whether the post is collapsible or never collapsed.
|
|
||||||
*/
|
|
||||||
val isCollapsible: Boolean,
|
|
||||||
/**
|
/**
|
||||||
* Specifies whether the content of this post is currently limited in visibility to the first
|
* Specifies whether the content of this post is currently limited in visibility to the first
|
||||||
* 500 characters or not.
|
* 500 characters or not.
|
||||||
|
@ -51,6 +46,14 @@ sealed class StatusViewData {
|
||||||
override val id: String
|
override val id: String
|
||||||
get() = status.id
|
get() = status.id
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specifies whether the content of this post is allowed to be collapsed or if it should show
|
||||||
|
* all content regardless.
|
||||||
|
*
|
||||||
|
* @return Whether the post is collapsible or never collapsed.
|
||||||
|
*/
|
||||||
|
val isCollapsible: Boolean
|
||||||
|
|
||||||
val content: Spanned
|
val content: Spanned
|
||||||
val spoilerText: String
|
val spoilerText: String
|
||||||
val username: String
|
val username: String
|
||||||
|
@ -74,45 +77,17 @@ sealed class StatusViewData {
|
||||||
init {
|
init {
|
||||||
if (Build.VERSION.SDK_INT == 23) {
|
if (Build.VERSION.SDK_INT == 23) {
|
||||||
// https://github.com/tuskyapp/Tusky/issues/563
|
// https://github.com/tuskyapp/Tusky/issues/563
|
||||||
this.content = replaceCrashingCharacters(status.actionableStatus.content)
|
this.content = replaceCrashingCharacters(status.actionableStatus.content.parseAsMastodonHtml())
|
||||||
this.spoilerText =
|
this.spoilerText =
|
||||||
replaceCrashingCharacters(status.actionableStatus.spoilerText).toString()
|
replaceCrashingCharacters(status.actionableStatus.spoilerText).toString()
|
||||||
this.username =
|
this.username =
|
||||||
replaceCrashingCharacters(status.actionableStatus.account.username).toString()
|
replaceCrashingCharacters(status.actionableStatus.account.username).toString()
|
||||||
} else {
|
} else {
|
||||||
this.content = status.actionableStatus.content
|
this.content = status.actionableStatus.content.parseAsMastodonHtml()
|
||||||
this.spoilerText = status.actionableStatus.spoilerText
|
this.spoilerText = status.actionableStatus.spoilerText
|
||||||
this.username = status.actionableStatus.account.username
|
this.username = status.actionableStatus.account.username
|
||||||
}
|
}
|
||||||
}
|
this.isCollapsible = shouldTrimStatus(this.content)
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val SOFT_HYPHEN = '\u00ad'
|
|
||||||
private const val ASCII_HYPHEN = '-'
|
|
||||||
fun replaceCrashingCharacters(content: Spanned): Spanned {
|
|
||||||
return replaceCrashingCharacters(content as CharSequence) as Spanned
|
|
||||||
}
|
|
||||||
|
|
||||||
fun replaceCrashingCharacters(content: CharSequence): CharSequence? {
|
|
||||||
var replacing = false
|
|
||||||
var builder: SpannableStringBuilder? = null
|
|
||||||
val length = content.length
|
|
||||||
for (index in 0 until length) {
|
|
||||||
val character = content[index]
|
|
||||||
|
|
||||||
// If there are more than one or two, switch to a map
|
|
||||||
if (character == SOFT_HYPHEN) {
|
|
||||||
if (!replacing) {
|
|
||||||
replacing = true
|
|
||||||
builder = SpannableStringBuilder(content, 0, index)
|
|
||||||
}
|
|
||||||
builder!!.append(ASCII_HYPHEN)
|
|
||||||
} else if (replacing) {
|
|
||||||
builder!!.append(character)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return if (replacing) builder else content
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Helper for Java */
|
/** Helper for Java */
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
@ -31,8 +32,7 @@ import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Resource
|
import com.keylesspalace.tusky.util.Resource
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import kotlinx.coroutines.launch
|
||||||
import io.reactivex.rxjava3.kotlin.addTo
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
|
@ -40,9 +40,7 @@ import okhttp3.RequestBody.Companion.asRequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import retrofit2.Call
|
import retrofit2.HttpException
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -63,24 +61,20 @@ class EditProfileViewModel @Inject constructor(
|
||||||
|
|
||||||
private var oldProfileData: Account? = null
|
private var oldProfileData: Account? = null
|
||||||
|
|
||||||
private val disposables = CompositeDisposable()
|
fun obtainProfile() = viewModelScope.launch {
|
||||||
|
|
||||||
fun obtainProfile() {
|
|
||||||
if (profileData.value == null || profileData.value is Error) {
|
if (profileData.value == null || profileData.value is Error) {
|
||||||
|
|
||||||
profileData.postValue(Loading())
|
profileData.postValue(Loading())
|
||||||
|
|
||||||
mastodonApi.accountVerifyCredentials()
|
mastodonApi.accountVerifyCredentials().fold(
|
||||||
.subscribe(
|
{ profile ->
|
||||||
{ profile ->
|
oldProfileData = profile
|
||||||
oldProfileData = profile
|
profileData.postValue(Success(profile))
|
||||||
profileData.postValue(Success(profile))
|
},
|
||||||
},
|
{
|
||||||
{
|
profileData.postValue(Error())
|
||||||
profileData.postValue(Error())
|
}
|
||||||
}
|
)
|
||||||
)
|
|
||||||
.addTo(disposables)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,34 +145,34 @@ class EditProfileViewModel @Inject constructor(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mastodonApi.accountUpdateCredentials(
|
viewModelScope.launch {
|
||||||
displayName, note, locked, avatar, header,
|
mastodonApi.accountUpdateCredentials(
|
||||||
field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second
|
displayName, note, locked, avatar, header,
|
||||||
).enqueue(object : Callback<Account> {
|
field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second
|
||||||
override fun onResponse(call: Call<Account>, response: Response<Account>) {
|
).fold(
|
||||||
val newProfileData = response.body()
|
{ newProfileData ->
|
||||||
if (!response.isSuccessful || newProfileData == null) {
|
saveData.postValue(Success())
|
||||||
val errorResponse = response.errorBody()?.string()
|
eventHub.dispatch(ProfileEditedEvent(newProfileData))
|
||||||
val errorMsg = if (!errorResponse.isNullOrBlank()) {
|
},
|
||||||
try {
|
{ throwable ->
|
||||||
JSONObject(errorResponse).optString("error", null)
|
if (throwable is HttpException) {
|
||||||
} catch (e: JSONException) {
|
val errorResponse = throwable.response()?.errorBody()?.string()
|
||||||
|
val errorMsg = if (!errorResponse.isNullOrBlank()) {
|
||||||
|
try {
|
||||||
|
JSONObject(errorResponse).optString("error", "")
|
||||||
|
} catch (e: JSONException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
saveData.postValue(Error(errorMessage = errorMsg))
|
||||||
} else {
|
} else {
|
||||||
null
|
saveData.postValue(Error())
|
||||||
}
|
}
|
||||||
saveData.postValue(Error(errorMessage = errorMsg))
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
saveData.postValue(Success())
|
)
|
||||||
eventHub.dispatch(ProfileEditedEvent(newProfileData))
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFailure(call: Call<Account>, t: Throwable) {
|
|
||||||
saveData.postValue(Error())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// cache activity state for rotation change
|
// cache activity state for rotation change
|
||||||
|
@ -208,15 +202,11 @@ class EditProfileViewModel @Inject constructor(
|
||||||
return File(application.cacheDir, filename)
|
return File(application.cacheDir, filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
fun obtainInstance() = viewModelScope.launch {
|
||||||
disposables.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun obtainInstance() {
|
|
||||||
if (instanceData.value == null || instanceData.value is Error) {
|
if (instanceData.value == null || instanceData.value is Error) {
|
||||||
instanceData.postValue(Loading())
|
instanceData.postValue(Loading())
|
||||||
|
|
||||||
mastodonApi.getInstance().subscribe(
|
mastodonApi.getInstance().fold(
|
||||||
{ instance ->
|
{ instance ->
|
||||||
instanceData.postValue(Success(instance))
|
instanceData.postValue(Success(instance))
|
||||||
},
|
},
|
||||||
|
@ -224,7 +214,6 @@ class EditProfileViewModel @Inject constructor(
|
||||||
instanceData.postValue(Error())
|
instanceData.postValue(Error())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.addTo(disposables)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
13
app/src/main/res/drawable/bot_badge.xml
Normal file
13
app/src/main/res/drawable/bot_badge.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:pathData="M5.7407,0L18.2593,0A5.7407,5.7407 0,0 1,24 5.7407L24,18.2593A5.7407,5.7407 0,0 1,18.2593 24L5.7407,24A5.7407,5.7407 0,0 1,0 18.2593L0,5.7407A5.7407,5.7407 0,0 1,5.7407 0z"
|
||||||
|
android:fillAlpha="0.75"
|
||||||
|
android:fillColor="@color/botBadgeBackground" />
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/botBadgeForeground"
|
||||||
|
android:pathData="m12,3.1674a1.6059,1.6059 0,0 1,1.6059 1.6059c0,0.5942 -0.3212,1.1161 -0.803,1.3891v1.0198h0.803a5.6207,5.6207 0,0 1,5.6207 5.6207h0.803a0.803,0.803 0,0 1,0.803 0.803v2.4089a0.803,0.803 0,0 1,-0.803 0.803h-0.803v0.803a1.6059,1.6059 0,0 1,-1.6059 1.6059H6.3793A1.6059,1.6059 0,0 1,4.7733 17.6207V16.8178H3.9704A0.803,0.803 0,0 1,3.1674 16.0148V13.6059A0.803,0.803 0,0 1,3.9704 12.803H4.7733a5.6207,5.6207 0,0 1,5.6207 -5.6207h0.803V6.1625C10.7153,5.8894 10.3941,5.3675 10.3941,4.7733A1.6059,1.6059 0,0 1,12 3.1674M8.3867,12A2.0074,2.0074 0,0 0,6.3793 14.0074,2.0074 2.0074,0 0,0 8.3867,16.0148 2.0074,2.0074 0,0 0,10.3941 14.0074,2.0074 2.0074,0 0,0 8.3867,12m7.2267,0a2.0074,2.0074 0,0 0,-2.0074 2.0074,2.0074 2.0074,0 0,0 2.0074,2.0074 2.0074,2.0074 0,0 0,2.0074 -2.0074A2.0074,2.0074 0,0 0,15.6133 12Z" />
|
||||||
|
</vector>
|
|
@ -4,6 +4,6 @@
|
||||||
android:viewportHeight="24"
|
android:viewportHeight="24"
|
||||||
android:viewportWidth="24">
|
android:viewportWidth="24">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#000"
|
android:fillColor="@color/textColorTertiary"
|
||||||
android:pathData="M20,6C20.58,6 21.05,6.2 21.42,6.59C21.8,7 22,7.45 22,8V19C22,19.55 21.8,20 21.42,20.41C21.05,20.8 20.58,21 20,21H4C3.42,21 2.95,20.8 2.58,20.41C2.2,20 2,19.55 2,19V8C2,7.45 2.2,7 2.58,6.59C2.95,6.2 3.42,6 4,6H8V4C8,3.42 8.2,2.95 8.58,2.58C8.95,2.2 9.42,2 10,2H14C14.58,2 15.05,2.2 15.42,2.58C15.8,2.95 16,3.42 16,4V6H20M4,8V19H20V8H4M14,6V4H10V6H14Z" />
|
android:pathData="M20,6C20.58,6 21.05,6.2 21.42,6.59C21.8,7 22,7.45 22,8V19C22,19.55 21.8,20 21.42,20.41C21.05,20.8 20.58,21 20,21H4C3.42,21 2.95,20.8 2.58,20.41C2.2,20 2,19.55 2,19V8C2,7.45 2.2,7 2.58,6.59C2.95,6.2 3.42,6 4,6H8V4C8,3.42 8.2,2.95 8.58,2.58C8.95,2.2 9.42,2 10,2H14C14.58,2 15.05,2.2 15.42,2.58C15.8,2.95 16,3.42 16,4V6H20M4,8V19H20V8H4M14,6V4H10V6H14Z" />
|
||||||
</vector>
|
</vector>
|
10
app/src/main/res/drawable/ic_edit_24dp.xml
Normal file
10
app/src/main/res/drawable/ic_edit_24dp.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/>
|
||||||
|
</vector>
|
|
@ -112,7 +112,7 @@
|
||||||
app:layout_constraintStart_toStartOf="@id/guideAvatar"
|
app:layout_constraintStart_toStartOf="@id/guideAvatar"
|
||||||
app:layout_constraintTop_toTopOf="@+id/accountFollowButton" />
|
app:layout_constraintTop_toTopOf="@+id/accountFollowButton" />
|
||||||
|
|
||||||
<androidx.emoji.widget.EmojiTextView
|
<TextView
|
||||||
android:id="@+id/accountDisplayNameTextView"
|
android:id="@+id/accountDisplayNameTextView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -215,7 +215,7 @@
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/accountNoteTextInputLayout" />
|
app:layout_constraintTop_toBottomOf="@id/accountNoteTextInputLayout" />
|
||||||
|
|
||||||
<androidx.emoji.widget.EmojiTextView
|
<TextView
|
||||||
android:id="@+id/accountNoteTextView"
|
android:id="@+id/accountNoteTextView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -248,63 +248,71 @@
|
||||||
app:layout_constraintTop_toBottomOf="@id/accountFieldList"
|
app:layout_constraintTop_toBottomOf="@id/accountFieldList"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Group
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:id="@+id/accountMovedView"
|
android:id="@+id/accountMovedView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
app:constraint_referenced_ids="accountMovedText,accountMovedAvatar,accountMovedDisplayName,accountMovedUsername" />
|
android:layout_marginTop="4dp"
|
||||||
|
|
||||||
<androidx.emoji.widget.EmojiTextView
|
|
||||||
android:id="@+id/accountMovedText"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="12dp"
|
|
||||||
android:drawablePadding="6dp"
|
|
||||||
android:textSize="?attr/status_text_medium"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/accountRemoveView"
|
app:layout_constraintTop_toBottomOf="@id/accountRemoveView"
|
||||||
tools:text="Account has moved" />
|
tools:visibility="visible">
|
||||||
|
|
||||||
<ImageView
|
<TextView
|
||||||
android:id="@+id/accountMovedAvatar"
|
android:id="@+id/accountMovedText"
|
||||||
android:layout_width="48dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="48dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerVertical="true"
|
android:layout_marginTop="8dp"
|
||||||
android:layout_marginTop="8dp"
|
android:drawablePadding="6dp"
|
||||||
android:layout_marginEnd="24dp"
|
android:drawableStart="@drawable/ic_briefcase"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
android:textSize="?attr/status_text_medium"
|
||||||
app:layout_constraintTop_toBottomOf="@id/accountMovedText"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
tools:src="@drawable/avatar_default" />
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Account has moved" />
|
||||||
|
|
||||||
<androidx.emoji.widget.EmojiTextView
|
<ImageView
|
||||||
android:id="@+id/accountMovedDisplayName"
|
android:importantForAccessibility="no"
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/accountMovedAvatar"
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="48dp"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_height="48dp"
|
||||||
android:ellipsize="end"
|
android:layout_centerVertical="true"
|
||||||
android:maxLines="1"
|
android:layout_marginTop="8dp"
|
||||||
android:textColor="?android:textColorPrimary"
|
android:layout_marginBottom="8dp"
|
||||||
android:textSize="?attr/status_text_large"
|
android:layout_marginEnd="24dp"
|
||||||
android:textStyle="normal|bold"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintBottom_toTopOf="@id/accountMovedUsername"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/accountMovedAvatar"
|
app:layout_constraintTop_toBottomOf="@id/accountMovedText"
|
||||||
app:layout_constraintTop_toTopOf="@id/accountMovedAvatar"
|
tools:src="@drawable/avatar_default" />
|
||||||
tools:text="Display name" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/accountMovedUsername"
|
android:id="@+id/accountMovedDisplayName"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:textColor="?android:textColorSecondary"
|
android:textColor="?android:textColorPrimary"
|
||||||
android:textSize="?attr/status_text_medium"
|
android:textSize="?attr/status_text_large"
|
||||||
app:layout_constraintBottom_toBottomOf="@id/accountMovedAvatar"
|
android:textStyle="normal|bold"
|
||||||
app:layout_constraintStart_toEndOf="@id/accountMovedAvatar"
|
app:layout_constraintBottom_toTopOf="@id/accountMovedUsername"
|
||||||
app:layout_constraintTop_toBottomOf="@id/accountMovedDisplayName"
|
app:layout_constraintStart_toEndOf="@id/accountMovedAvatar"
|
||||||
tools:text="\@username" />
|
app:layout_constraintTop_toTopOf="@id/accountMovedAvatar"
|
||||||
|
tools:text="Display name" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/accountMovedUsername"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="16dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="?android:textColorSecondary"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintBottom_toBottomOf="@id/accountMovedAvatar"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/accountMovedAvatar"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/accountMovedDisplayName"
|
||||||
|
tools:text="\@username" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/accountStatuses"
|
android:id="@+id/accountStatuses"
|
||||||
|
@ -315,7 +323,7 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
app:layout_constraintEnd_toStartOf="@id/accountFollowing"
|
app:layout_constraintEnd_toStartOf="@id/accountFollowing"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@id/accountMovedAvatar">
|
app:layout_constraintTop_toBottomOf="@id/accountMovedView">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/accountStatusesTextView"
|
android:id="@+id/accountStatusesTextView"
|
||||||
|
@ -346,7 +354,7 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
app:layout_constraintEnd_toStartOf="@id/accountFollowers"
|
app:layout_constraintEnd_toStartOf="@id/accountFollowers"
|
||||||
app:layout_constraintStart_toEndOf="@id/accountStatuses"
|
app:layout_constraintStart_toEndOf="@id/accountStatuses"
|
||||||
app:layout_constraintTop_toBottomOf="@id/accountMovedAvatar">
|
app:layout_constraintTop_toBottomOf="@id/accountMovedView">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/accountFollowingTextView"
|
android:id="@+id/accountFollowingTextView"
|
||||||
|
@ -376,7 +384,7 @@
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/accountFollowing"
|
app:layout_constraintStart_toEndOf="@id/accountFollowing"
|
||||||
app:layout_constraintTop_toBottomOf="@id/accountMovedAvatar">
|
app:layout_constraintTop_toBottomOf="@id/accountMovedView">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/accountFollowersTextView"
|
android:id="@+id/accountFollowersTextView"
|
||||||
|
|
|
@ -72,7 +72,7 @@
|
||||||
tools:text="Reply to @username"
|
tools:text="Reply to @username"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<androidx.emoji.widget.EmojiTextView
|
<TextView
|
||||||
android:id="@+id/composeReplyContentView"
|
android:id="@+id/composeReplyContentView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -94,7 +94,7 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<androidx.emoji.widget.EmojiEditText
|
<androidx.emoji2.widget.EmojiEditText
|
||||||
android:id="@+id/composeContentWarningField"
|
android:id="@+id/composeContentWarningField"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -11,11 +11,16 @@
|
||||||
<androidx.appcompat.widget.Toolbar
|
<androidx.appcompat.widget.Toolbar
|
||||||
android:id="@+id/loginToolbar"
|
android:id="@+id/loginToolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content" />
|
||||||
app:titleTextColor="#fff" />
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/loginProgress"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center" />
|
||||||
|
|
||||||
<WebView
|
<WebView
|
||||||
android:id="@+id/loginWebView"
|
android:id="@+id/loginWebView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
|
@ -58,6 +58,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="?attr/actionBarSize"
|
android:layout_height="?attr/actionBarSize"
|
||||||
app:tabIndicator="@null"
|
app:tabIndicator="@null"
|
||||||
|
app:tabGravity="fill"
|
||||||
app:tabMode="fixed" />
|
app:tabMode="fixed" />
|
||||||
|
|
||||||
</com.google.android.material.bottomappbar.BottomAppBar>
|
</com.google.android.material.bottomappbar.BottomAppBar>
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingTop="16dp">
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/item_blobmoji"
|
|
||||||
layout="@layout/item_emoji_pref" />
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/item_twemoji"
|
|
||||||
layout="@layout/item_emoji_pref" />
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/item_notoemoji"
|
|
||||||
layout="@layout/item_emoji_pref" />
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/item_nomoji"
|
|
||||||
layout="@layout/item_emoji_pref" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/emoji_download_label"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:lineSpacingMultiplier="1.1"
|
|
||||||
android:paddingStart="24dp"
|
|
||||||
android:paddingTop="16dp"
|
|
||||||
android:paddingEnd="24dp"
|
|
||||||
android:paddingBottom="8dp"
|
|
||||||
android:text="@string/download_fonts"
|
|
||||||
android:textColor="?android:attr/textColorSecondary" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
|
@ -32,7 +32,7 @@
|
||||||
tools:src="#000"
|
tools:src="#000"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
<androidx.emoji.widget.EmojiTextView
|
<TextView
|
||||||
android:id="@+id/account_display_name"
|
android:id="@+id/account_display_name"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
android:paddingTop="4dp">
|
android:paddingTop="4dp">
|
||||||
|
|
||||||
<!-- 30% width for the field name, 70% for the value -->
|
<!-- 30% width for the field name, 70% for the value -->
|
||||||
<androidx.emoji.widget.EmojiTextView
|
<TextView
|
||||||
android:id="@+id/accountFieldName"
|
android:id="@+id/accountFieldName"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
app:layout_constraintWidth_percent=".3"
|
app:layout_constraintWidth_percent=".3"
|
||||||
tools:text="Field title" />
|
tools:text="Field title" />
|
||||||
|
|
||||||
<androidx.emoji.widget.EmojiTextView
|
<TextView
|
||||||
android:id="@+id/accountFieldValue"
|
android:id="@+id/accountFieldValue"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
<androidx.emoji.widget.EmojiTextView
|
<TextView
|
||||||
android:id="@+id/text"
|
android:id="@+id/text"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<androidx.emoji.widget.EmojiTextView
|
<TextView
|
||||||
android:id="@+id/display_name"
|
android:id="@+id/display_name"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<androidx.emoji.widget.EmojiTextView
|
<TextView
|
||||||
android:id="@+id/blocked_user_display_name"
|
android:id="@+id/blocked_user_display_name"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue