Merge remote-tracking branch 'tuskyapp/develop'
This commit is contained in:
commit
61bc887af5
|
@ -99,11 +99,12 @@ project.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.roomVersion = '2.2.1'
|
ext.lifecycleVersion = "2.1.0"
|
||||||
|
ext.roomVersion = '2.2.3'
|
||||||
ext.retrofitVersion = '2.6.0'
|
ext.retrofitVersion = '2.6.0'
|
||||||
ext.okhttpVersion = '4.2.2'
|
ext.okhttpVersion = '4.2.2'
|
||||||
ext.glideVersion = '4.10.0'
|
ext.glideVersion = '4.10.0'
|
||||||
ext.daggerVersion = '2.25.2'
|
ext.daggerVersion = '2.25.3'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
|
@ -116,26 +117,28 @@ dependencies {
|
||||||
|
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
|
|
||||||
implementation "androidx.core:core-ktx:1.2.0-beta01"
|
implementation "androidx.core:core-ktx:1.2.0-rc01"
|
||||||
implementation "androidx.appcompat:appcompat:1.1.0"
|
implementation "androidx.appcompat:appcompat:1.1.0"
|
||||||
implementation "androidx.fragment:fragment-ktx:1.1.0"
|
implementation "androidx.fragment:fragment-ktx:1.1.0"
|
||||||
implementation "androidx.browser:browser:1.0.0"
|
implementation "androidx.browser:browser:1.2.0"
|
||||||
implementation "androidx.recyclerview:recyclerview:1.0.0"
|
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
|
||||||
implementation "androidx.exifinterface:exifinterface:1.0.0"
|
implementation "androidx.recyclerview:recyclerview:1.1.0"
|
||||||
|
implementation "androidx.exifinterface:exifinterface:1.1.0"
|
||||||
implementation "androidx.cardview:cardview:1.0.0"
|
implementation "androidx.cardview:cardview:1.0.0"
|
||||||
implementation "androidx.preference:preference:1.1.0"
|
implementation "androidx.preference:preference:1.1.0"
|
||||||
implementation "androidx.sharetarget:sharetarget:1.0.0-beta01"
|
implementation "androidx.sharetarget:sharetarget:1.0.0-rc01"
|
||||||
implementation "androidx.emoji:emoji:1.0.0"
|
implementation "androidx.emoji:emoji:1.0.0"
|
||||||
implementation "androidx.emoji:emoji-appcompat:1.0.0"
|
implementation "androidx.emoji:emoji-appcompat:1.0.0"
|
||||||
implementation "androidx.lifecycle:lifecycle-extensions:2.1.0"
|
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycleVersion"
|
||||||
|
implementation "androidx.lifecycle:lifecycle-reactivestreams:$lifecycleVersion"
|
||||||
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
|
implementation "androidx.constraintlayout:constraintlayout:1.1.3"
|
||||||
implementation "androidx.paging:paging-runtime-ktx:2.1.0"
|
implementation "androidx.paging:paging-runtime-ktx:2.1.1"
|
||||||
implementation "androidx.viewpager2:viewpager2:1.0.0-rc01"
|
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||||
implementation "androidx.room:room-runtime:$roomVersion"
|
implementation "androidx.room:room-runtime:$roomVersion"
|
||||||
implementation "androidx.room:room-rxjava2:$roomVersion"
|
implementation "androidx.room:room-rxjava2:$roomVersion"
|
||||||
kapt "androidx.room:room-compiler:$roomVersion"
|
kapt "androidx.room:room-compiler:$roomVersion"
|
||||||
|
|
||||||
implementation "com.google.android.material:material:1.1.0-beta01"
|
implementation "com.google.android.material:material:1.1.0-rc01"
|
||||||
|
|
||||||
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"
|
||||||
|
@ -149,7 +152,7 @@ dependencies {
|
||||||
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
||||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
|
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion"
|
||||||
|
|
||||||
implementation "io.reactivex.rxjava2:rxjava:2.2.13"
|
implementation "io.reactivex.rxjava2:rxjava:2.2.16"
|
||||||
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
|
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
|
||||||
implementation "io.reactivex.rxjava2:rxkotlin:2.4.0"
|
implementation "io.reactivex.rxjava2:rxkotlin:2.4.0"
|
||||||
|
|
||||||
|
@ -162,7 +165,7 @@ dependencies {
|
||||||
implementation "com.google.dagger:dagger-android-support:$daggerVersion"
|
implementation "com.google.dagger:dagger-android-support:$daggerVersion"
|
||||||
kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
|
kapt "com.google.dagger:dagger-android-processor:$daggerVersion"
|
||||||
|
|
||||||
implementation "com.github.connyduck:sparkbutton:2.0.1"
|
implementation "com.github.connyduck:sparkbutton:3.0.0"
|
||||||
|
|
||||||
implementation "com.github.chrisbanes:PhotoView:2.3.0"
|
implementation "com.github.chrisbanes:PhotoView:2.3.0"
|
||||||
|
|
||||||
|
@ -182,7 +185,7 @@ dependencies {
|
||||||
|
|
||||||
testImplementation "androidx.test.ext:junit:1.1.1"
|
testImplementation "androidx.test.ext:junit:1.1.1"
|
||||||
testImplementation "org.robolectric:robolectric:4.3.1"
|
testImplementation "org.robolectric:robolectric:4.3.1"
|
||||||
testImplementation "org.mockito:mockito-inline:3.1.0"
|
testImplementation "org.mockito:mockito-inline:3.2.4"
|
||||||
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0"
|
||||||
|
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.1.1", {
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.1.1", {
|
||||||
|
|
|
@ -0,0 +1,729 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 21,
|
||||||
|
"identityHash": "7570c84ffeb4f90521f91dc7ef3e7da1",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "TootEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "text",
|
||||||
|
"columnName": "text",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "urls",
|
||||||
|
"columnName": "urls",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "descriptions",
|
||||||
|
"columnName": "descriptions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentWarning",
|
||||||
|
"columnName": "contentWarning",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToText",
|
||||||
|
"columnName": "inReplyToText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToUsername",
|
||||||
|
"columnName": "inReplyToUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"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, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` 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": "notificationsReblogged",
|
||||||
|
"columnName": "notificationsReblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFavorited",
|
||||||
|
"columnName": "notificationsFavorited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsPolls",
|
||||||
|
"columnName": "notificationsPolls",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationSound",
|
||||||
|
"columnName": "notificationSound",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationVibration",
|
||||||
|
"columnName": "notificationVibration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationLight",
|
||||||
|
"columnName": "notificationLight",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultPostPrivacy",
|
||||||
|
"columnName": "defaultPostPrivacy",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultMediaSensitivity",
|
||||||
|
"columnName": "defaultMediaSensitivity",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysShowSensitiveMedia",
|
||||||
|
"columnName": "alwaysShowSensitiveMedia",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysOpenSpoiler",
|
||||||
|
"columnName": "alwaysOpenSpoiler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaPreviewEnabled",
|
||||||
|
"columnName": "mediaPreviewEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastNotificationId",
|
||||||
|
"columnName": "lastNotificationId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activeNotifications",
|
||||||
|
"columnName": "activeNotifications",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tabPreferences",
|
||||||
|
"columnName": "tabPreferences",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFilter",
|
||||||
|
"columnName": "notificationsFilter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_AccountEntity_domain_accountId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"domain",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "InstanceEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "instance",
|
||||||
|
"columnName": "instance",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojiList",
|
||||||
|
"columnName": "emojiList",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maximumTootCharacters",
|
||||||
|
"columnName": "maximumTootCharacters",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptions",
|
||||||
|
"columnName": "maxPollOptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptionLength",
|
||||||
|
"columnName": "maxPollOptionLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"instance"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineStatusEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` 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": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "TimelineAccountEntity",
|
||||||
|
"onDelete": "NO ACTION",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineAccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "localUsername",
|
||||||
|
"columnName": "localUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatar",
|
||||||
|
"columnName": "avatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bot",
|
||||||
|
"columnName": "bot",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ConversationEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` 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.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.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, '7570c84ffeb4f90521f91dc7ef3e7da1')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
Before Width: | Height: | Size: 4.0 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
Before Width: | Height: | Size: 13 KiB |
|
@ -96,7 +96,7 @@
|
||||||
|
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name=".ComposeActivity"
|
android:name=".components.compose.ComposeActivity"
|
||||||
android:theme="@style/TuskyDialogActivityTheme"
|
android:theme="@style/TuskyDialogActivityTheme"
|
||||||
android:windowSoftInputMode="stateVisible|adjustResize"/>
|
android:windowSoftInputMode="stateVisible|adjustResize"/>
|
||||||
<activity
|
<activity
|
||||||
|
|
|
@ -19,8 +19,7 @@ import android.animation.ArgbEvaluator
|
||||||
import android.content.Context
|
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.*
|
||||||
import android.graphics.PorterDuff
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
@ -48,9 +47,12 @@ import com.google.android.material.snackbar.Snackbar
|
||||||
import com.google.android.material.tabs.TabLayout
|
import com.google.android.material.tabs.TabLayout
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
import com.google.android.material.tabs.TabLayoutMediator
|
||||||
import com.keylesspalace.tusky.adapter.AccountFieldAdapter
|
import com.keylesspalace.tusky.adapter.AccountFieldAdapter
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
import com.keylesspalace.tusky.entity.Field
|
||||||
|
import com.keylesspalace.tusky.entity.IdentityProof
|
||||||
import com.keylesspalace.tusky.entity.Relationship
|
import com.keylesspalace.tusky.entity.Relationship
|
||||||
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
import com.keylesspalace.tusky.interfaces.ActionButtonActivity
|
||||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||||
|
@ -117,7 +119,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
viewModel = ViewModelProviders.of(this, viewModelFactory)[AccountViewModel::class.java]
|
viewModel = ViewModelProviders.of(this, viewModelFactory)[AccountViewModel::class.java]
|
||||||
|
|
||||||
// Obtain information to fill out the profile.
|
// Obtain information to fill out the profile.
|
||||||
viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID))
|
viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!)
|
||||||
|
|
||||||
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
|
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false)
|
animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false)
|
||||||
|
@ -265,7 +267,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
|
|
||||||
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
|
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
|
||||||
|
|
||||||
if(verticalOffset == oldOffset) {
|
if (verticalOffset == oldOffset) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
oldOffset = verticalOffset
|
oldOffset = verticalOffset
|
||||||
|
@ -349,6 +351,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
|
viewModel.accountFieldData.observe(this, Observer<List<Either<IdentityProof, Field>>> {
|
||||||
|
accountFieldAdapter.fields = it
|
||||||
|
accountFieldAdapter.notifyDataSetChanged()
|
||||||
|
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -377,7 +384,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
val emojifiedNote = CustomEmojiHelper.emojifyText(account.note, account.emojis, accountNoteTextView)
|
val emojifiedNote = CustomEmojiHelper.emojifyText(account.note, account.emojis, accountNoteTextView)
|
||||||
LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this, false)
|
LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this, false)
|
||||||
|
|
||||||
accountFieldAdapter.fields = account.fields ?: emptyList()
|
// accountFieldAdapter.fields = account.fields ?: emptyList()
|
||||||
accountFieldAdapter.emojis = account.emojis ?: emptyList()
|
accountFieldAdapter.emojis = account.emojis ?: emptyList()
|
||||||
accountFieldAdapter.notifyDataSetChanged()
|
accountFieldAdapter.notifyDataSetChanged()
|
||||||
|
|
||||||
|
@ -471,7 +478,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
// this is necessary because API 19 can't handle vector compound drawables
|
// this is necessary because API 19 can't handle vector compound drawables
|
||||||
val movedIcon = ContextCompat.getDrawable(this, R.drawable.ic_briefcase)?.mutate()
|
val movedIcon = ContextCompat.getDrawable(this, R.drawable.ic_briefcase)?.mutate()
|
||||||
val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
||||||
movedIcon?.setColorFilter(textColor, PorterDuff.Mode.SRC_IN)
|
movedIcon?.colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
|
||||||
|
|
||||||
accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
|
accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
|
||||||
}
|
}
|
||||||
|
@ -693,9 +700,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
|
|
||||||
private fun mention() {
|
private fun mention() {
|
||||||
loadedAccount?.let {
|
loadedAccount?.let {
|
||||||
val intent = ComposeActivity.IntentBuilder()
|
val intent = ComposeActivity.startIntent(this,
|
||||||
.mentionedUsernames(setOf(it.username))
|
ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username)))
|
||||||
.build(this)
|
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -754,7 +760,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
R.id.action_report -> {
|
R.id.action_report -> {
|
||||||
if(loadedAccount != null) {
|
if (loadedAccount != null) {
|
||||||
startActivity(ReportActivity.getIntent(this, viewModel.accountId, loadedAccount!!.username))
|
startActivity(ReportActivity.getIntent(this, viewModel.accountId, loadedAccount!!.username))
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -20,14 +20,15 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.browser.customtabs.CustomTabsIntent
|
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.browser.customtabs.CustomTabsIntent
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.entity.AccessToken
|
import com.keylesspalace.tusky.entity.AccessToken
|
||||||
|
@ -338,9 +339,16 @@ class LoginActivity : BaseActivity(), Injectable {
|
||||||
private fun openInCustomTab(uri: Uri, context: Context): Boolean {
|
private fun openInCustomTab(uri: Uri, context: Context): Boolean {
|
||||||
|
|
||||||
val toolbarColor = ThemeUtils.getColor(context, R.attr.custom_tab_toolbar)
|
val toolbarColor = ThemeUtils.getColor(context, R.attr.custom_tab_toolbar)
|
||||||
val customTabsIntent = CustomTabsIntent.Builder()
|
val customTabsIntentBuilder = CustomTabsIntent.Builder()
|
||||||
.setToolbarColor(toolbarColor)
|
.setToolbarColor(toolbarColor)
|
||||||
.build()
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
|
customTabsIntentBuilder.setNavigationBarColor(
|
||||||
|
ThemeUtils.getColor(context, android.R.attr.navigationBarColor)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val customTabsIntent = customTabsIntentBuilder.build()
|
||||||
try {
|
try {
|
||||||
customTabsIntent.launchUrl(context, uri)
|
customTabsIntent.launchUrl(context, uri)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
|
|
|
@ -52,6 +52,7 @@ import com.keylesspalace.tusky.appstore.DrawerFooterClickedEvent;
|
||||||
import com.keylesspalace.tusky.appstore.EventHub;
|
import com.keylesspalace.tusky.appstore.EventHub;
|
||||||
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent;
|
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent;
|
||||||
import com.keylesspalace.tusky.appstore.ProfileEditedEvent;
|
import com.keylesspalace.tusky.appstore.ProfileEditedEvent;
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsRepository;
|
import com.keylesspalace.tusky.components.conversation.ConversationsRepository;
|
||||||
import com.keylesspalace.tusky.components.search.SearchActivity;
|
import com.keylesspalace.tusky.components.search.SearchActivity;
|
||||||
import com.keylesspalace.tusky.db.AccountEntity;
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
|
|
|
@ -22,21 +22,6 @@ import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.adapter.SavedTootAdapter;
|
|
||||||
import com.keylesspalace.tusky.appstore.EventHub;
|
|
||||||
import com.keylesspalace.tusky.appstore.StatusComposedEvent;
|
|
||||||
import com.keylesspalace.tusky.db.AppDatabase;
|
|
||||||
import com.keylesspalace.tusky.db.TootDao;
|
|
||||||
import com.keylesspalace.tusky.db.TootEntity;
|
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
|
||||||
import com.keylesspalace.tusky.util.SaveTootHelper;
|
|
||||||
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
|
@ -44,16 +29,35 @@ import androidx.lifecycle.Lifecycle;
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
import com.keylesspalace.tusky.adapter.SavedTootAdapter;
|
||||||
|
import com.keylesspalace.tusky.appstore.EventHub;
|
||||||
|
import com.keylesspalace.tusky.appstore.StatusComposedEvent;
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||||
|
import com.keylesspalace.tusky.db.AppDatabase;
|
||||||
|
import com.keylesspalace.tusky.db.TootDao;
|
||||||
|
import com.keylesspalace.tusky.db.TootEntity;
|
||||||
|
import com.keylesspalace.tusky.di.Injectable;
|
||||||
|
import com.keylesspalace.tusky.util.SaveTootHelper;
|
||||||
|
|
||||||
|
import java.lang.ref.WeakReference;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
|
||||||
|
import static com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions;
|
||||||
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
||||||
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
||||||
|
|
||||||
public final class SavedTootActivity extends BaseActivity implements SavedTootAdapter.SavedTootAction,
|
public final class SavedTootActivity extends BaseActivity implements SavedTootAdapter.SavedTootAction,
|
||||||
Injectable {
|
Injectable {
|
||||||
|
|
||||||
private SaveTootHelper saveTootHelper;
|
|
||||||
|
|
||||||
// ui
|
// ui
|
||||||
private SavedTootAdapter adapter;
|
private SavedTootAdapter adapter;
|
||||||
private TextView noContent;
|
private TextView noContent;
|
||||||
|
@ -66,13 +70,13 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
|
||||||
EventHub eventHub;
|
EventHub eventHub;
|
||||||
@Inject
|
@Inject
|
||||||
AppDatabase database;
|
AppDatabase database;
|
||||||
|
@Inject
|
||||||
|
SaveTootHelper saveTootHelper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
saveTootHelper = new SaveTootHelper(database.tootDao(), this);
|
|
||||||
|
|
||||||
eventHub.getEvents()
|
eventHub.getEvents()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.ofType(StatusComposedEvent.class)
|
.ofType(StatusComposedEvent.class)
|
||||||
|
@ -153,18 +157,32 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void click(int position, TootEntity item) {
|
public void click(int position, TootEntity item) {
|
||||||
Intent intent = new ComposeActivity.IntentBuilder()
|
Gson gson = new Gson();
|
||||||
.savedTootUid(item.getUid())
|
Type stringListType = new TypeToken<List<String>>() {}.getType();
|
||||||
.tootText(item.getText())
|
List<String> jsonUrls = gson.fromJson(item.getUrls(), stringListType);
|
||||||
.contentWarning(item.getContentWarning())
|
List<String> descriptions = gson.fromJson(item.getDescriptions(), stringListType);
|
||||||
.savedJsonUrls(item.getUrls())
|
|
||||||
.savedJsonDescriptions(item.getDescriptions())
|
ComposeOptions composeOptions = new ComposeOptions(
|
||||||
.inReplyToId(item.getInReplyToId())
|
item.getUid(),
|
||||||
.replyingStatusAuthor(item.getInReplyToUsername())
|
item.getText(),
|
||||||
.replyingStatusContent(item.getInReplyToText())
|
jsonUrls,
|
||||||
.visibility(item.getVisibility())
|
descriptions,
|
||||||
.poll(item.getPoll())
|
/*mentionedUsernames*/null,
|
||||||
.build(this);
|
item.getInReplyToId(),
|
||||||
|
/*quoteId*/null,
|
||||||
|
/*quoteUrl*/null,
|
||||||
|
/*replyVisibility*/null,
|
||||||
|
item.getVisibility(),
|
||||||
|
item.getContentWarning(),
|
||||||
|
item.getInReplyToUsername(),
|
||||||
|
item.getInReplyToText(),
|
||||||
|
/*mediaAttachments*/null,
|
||||||
|
/*scheduledAt*/null,
|
||||||
|
/*sensitive*/null,
|
||||||
|
/*poll*/null,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
Intent intent = ComposeActivity.startIntent(this, composeOptions);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.MenuItem
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
@ -11,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.keylesspalace.tusky.adapter.ScheduledTootAdapter
|
import com.keylesspalace.tusky.adapter.ScheduledTootAdapter
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
@ -79,6 +81,16 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootAdapter.ScheduledToot
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
onBackPressed()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
fun loadStatuses() {
|
fun loadStatuses() {
|
||||||
progress_bar.visibility = View.VISIBLE
|
progress_bar.visibility = View.VISIBLE
|
||||||
mastodonApi.scheduledStatuses()
|
mastodonApi.scheduledStatuses()
|
||||||
|
@ -135,15 +147,15 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootAdapter.ScheduledToot
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val intent = ComposeActivity.IntentBuilder()
|
val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions(
|
||||||
.tootText(item.params.text)
|
tootText = item.params.text,
|
||||||
.contentWarning(item.params.spoilerText)
|
contentWarning = item.params.spoilerText,
|
||||||
.mediaAttachments(item.mediaAttachments)
|
mediaAttachments = item.mediaAttachments,
|
||||||
.inReplyToId(item.params.inReplyToId)
|
inReplyToId = item.params.inReplyToId,
|
||||||
.visibility(item.params.visibility)
|
visibility = item.params.visibility,
|
||||||
.scheduledAt(item.scheduledAt)
|
scheduledAt = item.scheduledAt,
|
||||||
.sensitive(item.params.sensitive)
|
sensitive = item.params.sensitive
|
||||||
.build(this)
|
))
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
delete(position, item)
|
delete(position, item)
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,6 +41,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
import kotlinx.android.synthetic.main.activity_tab_preference.*
|
import kotlinx.android.synthetic.main.activity_tab_preference.*
|
||||||
import kotlinx.android.synthetic.main.toolbar_basic.*
|
import kotlinx.android.synthetic.main.toolbar_basic.*
|
||||||
|
import kotlinx.android.synthetic.main.item_tab_preference.view.removeButton
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -76,7 +77,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
}
|
}
|
||||||
|
|
||||||
currentTabs = (accountManager.activeAccount?.tabPreferences ?: emptyList()).toMutableList()
|
currentTabs = (accountManager.activeAccount?.tabPreferences ?: emptyList()).toMutableList()
|
||||||
currentTabsAdapter = TabAdapter(currentTabs, false, this)
|
currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT)
|
||||||
currentTabsRecyclerView.adapter = currentTabsAdapter
|
currentTabsRecyclerView.adapter = currentTabsAdapter
|
||||||
currentTabsRecyclerView.layoutManager = LinearLayoutManager(this)
|
currentTabsRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
currentTabsRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
currentTabsRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
||||||
|
@ -109,10 +110,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||||
currentTabs.removeAt(viewHolder.adapterPosition)
|
onTabRemoved(viewHolder.adapterPosition)
|
||||||
currentTabsAdapter.notifyItemRemoved(viewHolder.adapterPosition)
|
|
||||||
updateAvailableTabs()
|
|
||||||
saveTabs()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||||
|
@ -168,6 +166,13 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
saveTabs()
|
saveTabs()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTabRemoved(position: Int) {
|
||||||
|
currentTabs.removeAt(position)
|
||||||
|
currentTabsAdapter.notifyItemRemoved(position)
|
||||||
|
updateAvailableTabs()
|
||||||
|
saveTabs()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onActionChipClicked(tab: TabData) {
|
override fun onActionChipClicked(tab: TabData) {
|
||||||
showEditHashtagDialog(tab)
|
showEditHashtagDialog(tab)
|
||||||
}
|
}
|
||||||
|
@ -273,7 +278,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
||||||
addTabAdapter.updateData(addableTabs)
|
addTabAdapter.updateData(addableTabs)
|
||||||
|
|
||||||
maxTabsInfo.visible(addableTabs.size == 0 || currentTabs.size >= MAX_TAB_COUNT)
|
maxTabsInfo.visible(addableTabs.size == 0 || currentTabs.size >= MAX_TAB_COUNT)
|
||||||
|
currentTabsAdapter.setRemoveButtonVisible(currentTabs.size > MIN_TAB_COUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartDelete(viewHolder: RecyclerView.ViewHolder) {
|
override fun onStartDelete(viewHolder: RecyclerView.ViewHolder) {
|
||||||
|
|
|
@ -72,7 +72,7 @@ public class TuskyApplication extends Application implements HasAndroidInjector
|
||||||
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13,
|
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13,
|
||||||
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
|
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
|
||||||
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
|
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
|
||||||
AppDatabase.MIGRATION_19_20)
|
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21)
|
||||||
.build();
|
.build();
|
||||||
accountManager = new AccountManager(appDatabase);
|
accountManager = new AccountManager(appDatabase);
|
||||||
serviceLocator = new ServiceLocator() {
|
serviceLocator = new ServiceLocator() {
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.adapter
|
package com.keylesspalace.tusky.adapter
|
||||||
|
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
@ -23,15 +24,17 @@ import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
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.CustomEmojiHelper
|
import com.keylesspalace.tusky.util.CustomEmojiHelper
|
||||||
|
import com.keylesspalace.tusky.util.Either
|
||||||
import com.keylesspalace.tusky.util.LinkHelper
|
import com.keylesspalace.tusky.util.LinkHelper
|
||||||
import kotlinx.android.synthetic.main.item_account_field.view.*
|
import kotlinx.android.synthetic.main.item_account_field.view.*
|
||||||
|
|
||||||
class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView.Adapter<AccountFieldAdapter.ViewHolder>() {
|
class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView.Adapter<AccountFieldAdapter.ViewHolder>() {
|
||||||
|
|
||||||
var emojis: List<Emoji> = emptyList()
|
var emojis: List<Emoji> = emptyList()
|
||||||
var fields: List<Field> = emptyList()
|
var fields: List<Either<IdentityProof, Field>> = emptyList()
|
||||||
|
|
||||||
override fun getItemCount() = fields.size
|
override fun getItemCount() = fields.size
|
||||||
|
|
||||||
|
@ -41,18 +44,30 @@ class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
|
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
|
||||||
val field = fields[position]
|
val proofOrField = fields[position]
|
||||||
|
|
||||||
val emojifiedName = CustomEmojiHelper.emojifyString(field.name, emojis, viewHolder.nameTextView)
|
if(proofOrField.isLeft()) {
|
||||||
viewHolder.nameTextView.text = emojifiedName
|
val identityProof = proofOrField.asLeft()
|
||||||
|
|
||||||
val emojifiedValue = CustomEmojiHelper.emojifyText(field.value, emojis, viewHolder.valueTextView)
|
viewHolder.nameTextView.text = identityProof.provider
|
||||||
LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener, false)
|
viewHolder.valueTextView.text = LinkHelper.createClickableText(identityProof.username, identityProof.profileUrl)
|
||||||
|
|
||||||
|
viewHolder.valueTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||||
|
|
||||||
if(field.verifiedAt != null) {
|
|
||||||
viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
||||||
} else {
|
} else {
|
||||||
viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0 )
|
val field = proofOrField.asRight()
|
||||||
|
val emojifiedName = CustomEmojiHelper.emojifyString(field.name, emojis, viewHolder.nameTextView)
|
||||||
|
viewHolder.nameTextView.text = emojifiedName
|
||||||
|
|
||||||
|
val emojifiedValue = CustomEmojiHelper.emojifyText(field.value, emojis, viewHolder.valueTextView)
|
||||||
|
LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener, false)
|
||||||
|
|
||||||
|
if(field.verifiedAt != null) {
|
||||||
|
viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0)
|
||||||
|
} else {
|
||||||
|
viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0 )
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -189,12 +189,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
@Nullable String spoilerText,
|
@Nullable String spoilerText,
|
||||||
@Nullable Status.Mention[] mentions,
|
@Nullable Status.Mention[] mentions,
|
||||||
@NonNull List<Emoji> emojis,
|
@NonNull List<Emoji> emojis,
|
||||||
|
@Nullable PollViewData poll,
|
||||||
final StatusActionListener listener,
|
final StatusActionListener listener,
|
||||||
boolean removeQuote) {
|
boolean removeQuote) {
|
||||||
if (TextUtils.isEmpty(spoilerText)) {
|
if (TextUtils.isEmpty(spoilerText)) {
|
||||||
contentWarningDescription.setVisibility(View.GONE);
|
contentWarningDescription.setVisibility(View.GONE);
|
||||||
contentWarningButton.setVisibility(View.GONE);
|
contentWarningButton.setVisibility(View.GONE);
|
||||||
this.setTextVisible(true, content, mentions, emojis, listener, removeQuote);
|
this.setTextVisible(true, content, mentions, emojis, poll, listener, removeQuote);
|
||||||
} else {
|
} else {
|
||||||
CharSequence emojiSpoiler = CustomEmojiHelper.emojifyString(spoilerText, emojis, contentWarningDescription);
|
CharSequence emojiSpoiler = CustomEmojiHelper.emojifyString(spoilerText, emojis, contentWarningDescription);
|
||||||
contentWarningDescription.setText(emojiSpoiler);
|
contentWarningDescription.setText(emojiSpoiler);
|
||||||
|
@ -206,9 +207,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||||
listener.onExpandedChange(isChecked, getAdapterPosition());
|
listener.onExpandedChange(isChecked, getAdapterPosition());
|
||||||
}
|
}
|
||||||
this.setTextVisible(isChecked, content, mentions, emojis, listener, removeQuote);
|
this.setTextVisible(isChecked, content, mentions, emojis, poll, listener, removeQuote);
|
||||||
});
|
});
|
||||||
this.setTextVisible(expanded, content, mentions, emojis, listener, removeQuote);
|
this.setTextVisible(expanded, content, mentions, emojis, poll, listener, removeQuote);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,11 +217,15 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
Spanned content,
|
Spanned content,
|
||||||
Status.Mention[] mentions,
|
Status.Mention[] mentions,
|
||||||
List<Emoji> emojis,
|
List<Emoji> emojis,
|
||||||
|
@Nullable PollViewData poll,
|
||||||
final StatusActionListener listener,
|
final StatusActionListener listener,
|
||||||
boolean removeQuote) {
|
boolean removeQuote) {
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, this.content);
|
Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, this.content);
|
||||||
LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener, removeQuote);
|
LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener, removeQuote);
|
||||||
|
if (poll != null) {
|
||||||
|
setupPoll(poll, emojis, listener);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LinkHelper.setClickableMentions(this.content, mentions, listener);
|
LinkHelper.setClickableMentions(this.content, mentions, listener);
|
||||||
}
|
}
|
||||||
|
@ -229,6 +234,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
} else {
|
} else {
|
||||||
this.content.setVisibility(View.VISIBLE);
|
this.content.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
setPollVisible(poll != null && expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPollVisible(boolean visible) {
|
||||||
|
int visibility = visible ? View.VISIBLE : View.GONE;
|
||||||
|
pollButton.setVisibility(visibility);
|
||||||
|
pollDescription.setVisibility(visibility);
|
||||||
|
pollOptions.setVisibility(visibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setAvatar(String url,
|
private void setAvatar(String url,
|
||||||
|
@ -663,40 +676,18 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
replyButton.setEnabled(!isNotestock);
|
replyButton.setEnabled(!isNotestock);
|
||||||
replyButton.setClickable(!isNotestock);
|
replyButton.setClickable(!isNotestock);
|
||||||
if (reblogButton != null) {
|
if (reblogButton != null) {
|
||||||
reblogButton.setEventListener(new SparkEventListener() {
|
reblogButton.setEventListener((button, buttonState) -> {
|
||||||
@Override
|
int position = getAdapterPosition();
|
||||||
public void onEvent(ImageView button, boolean buttonState) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
int position = getAdapterPosition();
|
listener.onReblog(buttonState, position);
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
|
||||||
listener.onReblog(buttonState, position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEventAnimationEnd(ImageView button, boolean buttonState) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEventAnimationStart(ImageView button, boolean buttonState) {
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
favouriteButton.setEventListener(new SparkEventListener() {
|
favouriteButton.setEventListener((button, buttonState) -> {
|
||||||
@Override
|
int position = getAdapterPosition();
|
||||||
public void onEvent(ImageView button, boolean buttonState) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
int position = getAdapterPosition();
|
listener.onFavourite(buttonState, position);
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
|
||||||
listener.onFavourite(buttonState, position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEventAnimationEnd(ImageView button, boolean buttonState) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEventAnimationStart(ImageView button, boolean buttonState) {
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -712,23 +703,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bookmarkButton.setEventListener(new SparkEventListener() {
|
bookmarkButton.setEventListener((button, buttonState) -> {
|
||||||
@Override
|
int position = getAdapterPosition();
|
||||||
public void onEvent(ImageView button, boolean buttonState) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
int position = getAdapterPosition();
|
listener.onBookmark(buttonState, position);
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
|
||||||
listener.onBookmark(buttonState, position);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEventAnimationEnd(ImageView button, boolean buttonState) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onEventAnimationStart(ImageView button, boolean buttonState) {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -802,13 +780,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
setRebloggingEnabled(status.getRebloggingEnabled() && !status.isNotestock(), status.getVisibility());
|
setRebloggingEnabled(status.getRebloggingEnabled() && !status.isNotestock(), status.getVisibility());
|
||||||
setQuoteEnabled(status.getRebloggingEnabled() && !status.isNotestock(), status.getVisibility());
|
setQuoteEnabled(status.getRebloggingEnabled() && !status.isNotestock(), status.getVisibility());
|
||||||
|
|
||||||
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), listener,
|
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), status.getPoll(), listener, status.getQuote() != null);
|
||||||
status.getQuote() != null);
|
|
||||||
|
|
||||||
setDescriptionForStatus(status);
|
setDescriptionForStatus(status);
|
||||||
|
|
||||||
setupPoll(status.getPoll(), status.getStatusEmojis(), listener);
|
|
||||||
|
|
||||||
// Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0
|
// Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0
|
||||||
// RecyclerView tries to set AccessibilityDelegateCompat to null
|
// RecyclerView tries to set AccessibilityDelegateCompat to null
|
||||||
// but ViewCompat code replaces is with the default one. RecyclerView never
|
// but ViewCompat code replaces is with the default one. RecyclerView never
|
||||||
|
@ -963,55 +938,44 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setupPoll(PollViewData poll, List<Emoji> emojis, StatusActionListener listener) {
|
private void setupPoll(PollViewData poll, List<Emoji> emojis, StatusActionListener listener) {
|
||||||
if (poll == null) {
|
long timestamp = System.currentTimeMillis();
|
||||||
|
|
||||||
pollOptions.setVisibility(View.GONE);
|
boolean expired = poll.getExpired() || (poll.getExpiresAt() != null && timestamp > poll.getExpiresAt().getTime());
|
||||||
|
|
||||||
pollDescription.setVisibility(View.GONE);
|
Context context = pollDescription.getContext();
|
||||||
|
|
||||||
|
pollOptions.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
if (expired || poll.getVoted()) {
|
||||||
|
// no voting possible
|
||||||
|
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), emojis, PollAdapter.RESULT);
|
||||||
|
|
||||||
pollButton.setVisibility(View.GONE);
|
pollButton.setVisibility(View.GONE);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
long timestamp = System.currentTimeMillis();
|
// voting possible
|
||||||
|
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE);
|
||||||
|
|
||||||
boolean expired = poll.getExpired() || (poll.getExpiresAt() != null && timestamp > poll.getExpiresAt().getTime());
|
pollButton.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
Context context = pollDescription.getContext();
|
pollButton.setOnClickListener(v -> {
|
||||||
|
|
||||||
pollOptions.setVisibility(View.VISIBLE);
|
int position = getAdapterPosition();
|
||||||
|
|
||||||
if (expired || poll.getVoted()) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
// no voting possible
|
|
||||||
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), emojis, PollAdapter.RESULT);
|
|
||||||
|
|
||||||
pollButton.setVisibility(View.GONE);
|
List<Integer> pollResult = pollAdapter.getSelected();
|
||||||
} else {
|
|
||||||
// voting possible
|
|
||||||
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE);
|
|
||||||
|
|
||||||
pollButton.setVisibility(View.VISIBLE);
|
if (!pollResult.isEmpty()) {
|
||||||
|
listener.onVoteInPoll(position, pollResult);
|
||||||
pollButton.setOnClickListener(v -> {
|
|
||||||
|
|
||||||
int position = getAdapterPosition();
|
|
||||||
|
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
|
||||||
|
|
||||||
List<Integer> pollResult = pollAdapter.getSelected();
|
|
||||||
|
|
||||||
if (!pollResult.isEmpty()) {
|
|
||||||
listener.onVoteInPoll(position, pollResult);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
pollDescription.setVisibility(View.VISIBLE);
|
|
||||||
pollDescription.setText(getPollInfoText(timestamp, poll, context));
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pollDescription.setVisibility(View.VISIBLE);
|
||||||
|
pollDescription.setText(getPollInfoText(timestamp, poll, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
private CharSequence getPollInfoText(long timestamp, PollViewData poll, Context context) {
|
private CharSequence getPollInfoText(long timestamp, PollViewData poll, Context context) {
|
||||||
|
|
|
@ -118,10 +118,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
timestampInfo.append(" • ");
|
timestampInfo.append(" • ");
|
||||||
|
|
||||||
if (app.getWebsite() != null) {
|
if (app.getWebsite() != null) {
|
||||||
URLSpan span = new CustomURLSpan(app.getWebsite());
|
CharSequence text = LinkHelper.createClickableText(app.getName(), app.getWebsite());
|
||||||
|
|
||||||
SpannableStringBuilder text = new SpannableStringBuilder(app.getName());
|
|
||||||
text.setSpan(span, 0, app.getName().length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
|
||||||
timestampInfo.append(text);
|
timestampInfo.append(text);
|
||||||
timestampInfo.setMovementMethod(LinkMovementMethod.getInstance());
|
timestampInfo.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -32,14 +32,16 @@ import kotlinx.android.synthetic.main.item_tab_preference.view.*
|
||||||
|
|
||||||
interface ItemInteractionListener {
|
interface ItemInteractionListener {
|
||||||
fun onTabAdded(tab: TabData)
|
fun onTabAdded(tab: TabData)
|
||||||
|
fun onTabRemoved(position: Int)
|
||||||
fun onStartDelete(viewHolder: RecyclerView.ViewHolder)
|
fun onStartDelete(viewHolder: RecyclerView.ViewHolder)
|
||||||
fun onStartDrag(viewHolder: RecyclerView.ViewHolder)
|
fun onStartDrag(viewHolder: RecyclerView.ViewHolder)
|
||||||
fun onActionChipClicked(tab: TabData)
|
fun onActionChipClicked(tab: TabData)
|
||||||
}
|
}
|
||||||
|
|
||||||
class TabAdapter(private var data: List<TabData>,
|
class TabAdapter(private var data: List<TabData>,
|
||||||
private val small: Boolean = false,
|
private val small: Boolean,
|
||||||
private val listener: ItemInteractionListener? = null) : RecyclerView.Adapter<TabAdapter.ViewHolder>() {
|
private val listener: ItemInteractionListener,
|
||||||
|
private var removeButtonEnabled: Boolean = false) : RecyclerView.Adapter<TabAdapter.ViewHolder>() {
|
||||||
|
|
||||||
fun updateData(newData: List<TabData>) {
|
fun updateData(newData: List<TabData>) {
|
||||||
this.data = newData
|
this.data = newData
|
||||||
|
@ -67,17 +69,28 @@ class TabAdapter(private var data: List<TabData>,
|
||||||
holder.itemView.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(iconDrawable, null, null, null)
|
holder.itemView.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(iconDrawable, null, null, null)
|
||||||
if (small) {
|
if (small) {
|
||||||
holder.itemView.textView.setOnClickListener {
|
holder.itemView.textView.setOnClickListener {
|
||||||
listener?.onTabAdded(data[position])
|
listener.onTabAdded(data[position])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
holder.itemView.imageView?.setOnTouchListener { _, event ->
|
holder.itemView.imageView?.setOnTouchListener { _, event ->
|
||||||
if (event.action == MotionEvent.ACTION_DOWN) {
|
if (event.action == MotionEvent.ACTION_DOWN) {
|
||||||
listener?.onStartDrag(holder)
|
listener.onStartDrag(holder)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
holder.itemView.removeButton?.setOnClickListener {
|
||||||
|
listener.onTabRemoved(holder.adapterPosition)
|
||||||
|
}
|
||||||
|
if (holder.itemView.removeButton != null) {
|
||||||
|
holder.itemView.removeButton.isEnabled = removeButtonEnabled
|
||||||
|
ThemeUtils.setDrawableTint(
|
||||||
|
holder.itemView.context,
|
||||||
|
holder.itemView.removeButton.drawable,
|
||||||
|
(if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.image_button_disabled_tint)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!small) {
|
if (!small) {
|
||||||
|
|
||||||
|
@ -89,7 +102,7 @@ class TabAdapter(private var data: List<TabData>,
|
||||||
|
|
||||||
holder.itemView.actionChip.chipIcon = context.getDrawable(R.drawable.ic_edit_chip)
|
holder.itemView.actionChip.chipIcon = context.getDrawable(R.drawable.ic_edit_chip)
|
||||||
holder.itemView.actionChip.setOnClickListener {
|
holder.itemView.actionChip.setOnClickListener {
|
||||||
listener?.onActionChipClicked(data[position])
|
listener.onActionChipClicked(data[position])
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -102,5 +115,12 @@ class TabAdapter(private var data: List<TabData>,
|
||||||
return data.size
|
return data.size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setRemoveButtonVisible(enabled: Boolean) {
|
||||||
|
if (removeButtonEnabled != enabled) {
|
||||||
|
removeButtonEnabled = enabled
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,498 @@
|
||||||
|
/* Copyright 2019 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.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.Observer
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||||
|
import com.keylesspalace.tusky.components.search.SearchType
|
||||||
|
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.Emoji
|
||||||
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.service.ServiceClient
|
||||||
|
import com.keylesspalace.tusky.service.TootToSend
|
||||||
|
import com.keylesspalace.tusky.util.*
|
||||||
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.disposables.Disposable
|
||||||
|
import io.reactivex.rxkotlin.Singles
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
open class RxAwareViewModel : ViewModel() {
|
||||||
|
private val disposables = CompositeDisposable()
|
||||||
|
|
||||||
|
fun Disposable.autoDispose() = disposables.add(this)
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
disposables.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw when trying to add an image when video is already present or the other way around
|
||||||
|
*/
|
||||||
|
class VideoOrImageException : Exception()
|
||||||
|
|
||||||
|
|
||||||
|
class ComposeViewModel
|
||||||
|
@Inject constructor(
|
||||||
|
private val api: MastodonApi,
|
||||||
|
private val accountManager: AccountManager,
|
||||||
|
private val mediaUploader: MediaUploader,
|
||||||
|
private val serviceClient: ServiceClient,
|
||||||
|
private val saveTootHelper: SaveTootHelper,
|
||||||
|
private val db: AppDatabase
|
||||||
|
) : RxAwareViewModel() {
|
||||||
|
|
||||||
|
private var replyingStatusAuthor: String? = null
|
||||||
|
private var replyingStatusContent: String? = null
|
||||||
|
internal var startingText: String? = null
|
||||||
|
private var savedTootUid: Int = 0
|
||||||
|
private var startingContentWarning: String? = null
|
||||||
|
private var inReplyToId: String? = null
|
||||||
|
private var quoteId: String? = null
|
||||||
|
private var quoteUrl: String? = null
|
||||||
|
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
|
||||||
|
|
||||||
|
private val instance: MutableLiveData<InstanceEntity?> = 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,
|
||||||
|
supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val emoji: MutableLiveData<List<Emoji>?> = MutableLiveData()
|
||||||
|
val markMediaAsSensitive =
|
||||||
|
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
||||||
|
|
||||||
|
fun toggleMarkSensitive() {
|
||||||
|
this.markMediaAsSensitive.value = !this.markMediaAsSensitive.value!!
|
||||||
|
}
|
||||||
|
|
||||||
|
val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN)
|
||||||
|
val showContentWarning = mutableLiveData(false)
|
||||||
|
val poll: MutableLiveData<NewPoll?> = mutableLiveData(null)
|
||||||
|
val scheduledAt: MutableLiveData<String?> = mutableLiveData(null)
|
||||||
|
|
||||||
|
val media = mutableLiveData<List<QueuedMedia>>(listOf())
|
||||||
|
val uploadError = MutableLiveData<Throwable>()
|
||||||
|
|
||||||
|
val domain = accountManager.activeAccount?.domain!!
|
||||||
|
|
||||||
|
private val mediaToDisposable = mutableMapOf<Long, Disposable>()
|
||||||
|
|
||||||
|
|
||||||
|
fun loadInstanceDataFromNetwork() {
|
||||||
|
|
||||||
|
Singles.zip(api.getCustomEmojis(), api.getInstance()) { emojis, instance ->
|
||||||
|
InstanceEntity(
|
||||||
|
instance = domain,
|
||||||
|
emojiList = emojis,
|
||||||
|
maximumTootCharacters = instance.maxTootChars,
|
||||||
|
maxPollOptions = instance.pollLimits?.maxOptions,
|
||||||
|
maxPollOptionLength = instance.pollLimits?.maxOptionChars,
|
||||||
|
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 loadInstanceDataFromCache() {
|
||||||
|
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): LiveData<Either<Throwable, QueuedMedia>> {
|
||||||
|
// We are not calling .toLiveData() here because we don't want to stop the process when
|
||||||
|
// the Activity goes away temporarily (like on screen rotation).
|
||||||
|
val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>()
|
||||||
|
mediaUploader.prepareMedia(uri)
|
||||||
|
.map { (type, uri, size) ->
|
||||||
|
val mediaItems = media.value!!
|
||||||
|
if (type == QueuedMedia.Type.VIDEO
|
||||||
|
&& mediaItems.isNotEmpty()
|
||||||
|
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) {
|
||||||
|
throw VideoOrImageException()
|
||||||
|
} else {
|
||||||
|
addMediaToQueue(type, uri, size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.subscribe({ queuedMedia ->
|
||||||
|
liveData.postValue(Either.Right(queuedMedia))
|
||||||
|
}, { error ->
|
||||||
|
liveData.postValue(Either.Left(error))
|
||||||
|
})
|
||||||
|
.autoDispose()
|
||||||
|
return liveData
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addMediaToQueue(type: QueuedMedia.Type, uri: Uri, mediaSize: Long): QueuedMedia {
|
||||||
|
val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, mediaSize)
|
||||||
|
media.value = media.value!! + mediaItem
|
||||||
|
mediaToDisposable[mediaItem.localId] = mediaUploader
|
||||||
|
.uploadMedia(mediaItem)
|
||||||
|
.subscribe ({ event ->
|
||||||
|
val item = media.value?.find { it.localId == mediaItem.localId }
|
||||||
|
?: return@subscribe
|
||||||
|
val newMediaItem = when (event) {
|
||||||
|
is UploadEvent.ProgressEvent ->
|
||||||
|
item.copy(uploadPercent = event.percentage)
|
||||||
|
is UploadEvent.FinishedEvent ->
|
||||||
|
item.copy(id = event.attachment.id, uploadPercent = -1)
|
||||||
|
}
|
||||||
|
synchronized(media) {
|
||||||
|
val mediaValue = media.value!!
|
||||||
|
val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId }
|
||||||
|
media.postValue(if (index == -1) {
|
||||||
|
mediaValue + newMediaItem
|
||||||
|
} else {
|
||||||
|
mediaValue.toMutableList().also { it[index] = newMediaItem }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, { error ->
|
||||||
|
media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList())
|
||||||
|
uploadError.postValue(error)
|
||||||
|
})
|
||||||
|
return mediaItem
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?) {
|
||||||
|
val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, 0, -1, id, description)
|
||||||
|
media.value = media.value!! + mediaItem
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeMediaFromQueue(item: QueuedMedia) {
|
||||||
|
mediaToDisposable[item.localId]?.dispose()
|
||||||
|
media.value = media.value!!.withoutFirstWhich { it.localId == item.localId }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun didChange(content: String?, contentWarning: String?): Boolean {
|
||||||
|
|
||||||
|
val textChanged = !(content.isNullOrEmpty()
|
||||||
|
|| startingText?.startsWith(content.toString()) ?: false)
|
||||||
|
|
||||||
|
val contentWarningChanged = showContentWarning.value!!
|
||||||
|
&& !contentWarning.isNullOrEmpty()
|
||||||
|
&& !startingContentWarning!!.startsWith(contentWarning.toString())
|
||||||
|
val mediaChanged = media.value!!.isNotEmpty()
|
||||||
|
val pollChanged = poll.value != null
|
||||||
|
|
||||||
|
return textChanged || contentWarningChanged || mediaChanged || pollChanged
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteDraft() {
|
||||||
|
saveTootHelper.deleteDraft(this.savedTootUid)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveDraft(content: String, contentWarning: String) {
|
||||||
|
val mediaUris = mutableListOf<String>()
|
||||||
|
val mediaDescriptions = mutableListOf<String?>()
|
||||||
|
for (item in media.value!!) {
|
||||||
|
mediaUris.add(item.uri.toString())
|
||||||
|
mediaDescriptions.add(item.description)
|
||||||
|
}
|
||||||
|
saveTootHelper.saveToot(
|
||||||
|
content,
|
||||||
|
contentWarning,
|
||||||
|
null,
|
||||||
|
mediaUris,
|
||||||
|
mediaDescriptions,
|
||||||
|
savedTootUid,
|
||||||
|
inReplyToId,
|
||||||
|
replyingStatusContent,
|
||||||
|
replyingStatusAuthor,
|
||||||
|
statusVisibility.value!!,
|
||||||
|
poll.value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send status to the server.
|
||||||
|
* Uses current state plus provided arguments.
|
||||||
|
* @return LiveData which will signal once the screen can be closed or null if there are errors
|
||||||
|
*/
|
||||||
|
fun sendStatus(
|
||||||
|
content: String,
|
||||||
|
spoilerText: String
|
||||||
|
): LiveData<Unit> {
|
||||||
|
return media
|
||||||
|
.filter { items -> items.all { it.uploadPercent == -1 } }
|
||||||
|
.map {
|
||||||
|
val mediaIds = ArrayList<String>()
|
||||||
|
val mediaUris = ArrayList<Uri>()
|
||||||
|
val mediaDescriptions = ArrayList<String>()
|
||||||
|
for (item in media.value!!) {
|
||||||
|
mediaIds.add(item.id!!)
|
||||||
|
mediaUris.add(item.uri)
|
||||||
|
mediaDescriptions.add(item.description ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = content
|
||||||
|
if (domain !in CAN_USE_QUOTE_ID && quoteId != null) {
|
||||||
|
text += "\n~~~~~~~~~~\n[$quoteUrl]"
|
||||||
|
quoteId = null
|
||||||
|
}
|
||||||
|
|
||||||
|
val tootToSend = TootToSend(
|
||||||
|
text,
|
||||||
|
spoilerText,
|
||||||
|
statusVisibility.value!!.serverString(),
|
||||||
|
mediaUris.isNotEmpty() && markMediaAsSensitive.value!!,
|
||||||
|
mediaIds,
|
||||||
|
mediaUris.map { it.toString() },
|
||||||
|
mediaDescriptions,
|
||||||
|
scheduledAt = scheduledAt.value,
|
||||||
|
inReplyToId = inReplyToId,
|
||||||
|
poll = poll.value,
|
||||||
|
replyingStatusContent = null,
|
||||||
|
replyingStatusAuthorUsername = null,
|
||||||
|
savedJsonUrls = null,
|
||||||
|
quoteId = quoteId,
|
||||||
|
accountId = accountManager.activeAccount!!.id,
|
||||||
|
savedTootUid = 0,
|
||||||
|
idempotencyKey = randomAlphanumericString(16),
|
||||||
|
retries = 0
|
||||||
|
)
|
||||||
|
serviceClient.sendToot(tootToSend)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDescription(localId: Long, description: String): LiveData<Boolean> {
|
||||||
|
val newList = media.value!!.toMutableList()
|
||||||
|
val index = newList.indexOfFirst { it.localId == localId }
|
||||||
|
if (index != -1) {
|
||||||
|
newList[index] = newList[index].copy(description = description)
|
||||||
|
}
|
||||||
|
media.value = newList
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
||||||
|
when (token[0]) {
|
||||||
|
'@' -> {
|
||||||
|
return try {
|
||||||
|
api.searchAccounts(query = token.substring(1), limit = 10)
|
||||||
|
.blockingGet()
|
||||||
|
.map { ComposeAutoCompleteAdapter.AccountResult(it) }
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'#' -> {
|
||||||
|
return try {
|
||||||
|
api.searchObservable(query = token, type = SearchType.Hashtag.apiParameter, limit = 10)
|
||||||
|
.blockingGet()
|
||||||
|
.hashtags
|
||||||
|
.map { ComposeAutoCompleteAdapter.HashtagResult(it) }
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
Log.e(TAG, String.format("Autocomplete search for %s failed.", token), e)
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
':' -> {
|
||||||
|
val emojiList = emoji.value ?: return emptyList()
|
||||||
|
|
||||||
|
val incomplete = token.substring(1).toLowerCase(Locale.ROOT)
|
||||||
|
val results = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
|
||||||
|
val resultsInside = ArrayList<ComposeAutoCompleteAdapter.AutocompleteResult>()
|
||||||
|
for (emoji in emojiList) {
|
||||||
|
val shortcode = emoji.shortcode.toLowerCase(Locale.ROOT)
|
||||||
|
if (shortcode.startsWith(incomplete)) {
|
||||||
|
results.add(ComposeAutoCompleteAdapter.EmojiResult(emoji))
|
||||||
|
} else if (shortcode.indexOf(incomplete, 1) != -1) {
|
||||||
|
resultsInside.add(ComposeAutoCompleteAdapter.EmojiResult(emoji))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (results.isNotEmpty() && resultsInside.isNotEmpty()) {
|
||||||
|
results.add(ComposeAutoCompleteAdapter.ResultSeparator())
|
||||||
|
}
|
||||||
|
results.addAll(resultsInside)
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.w(TAG, "Unexpected autocompletion token: $token")
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
for (uploadDisposable in mediaToDisposable.values) {
|
||||||
|
uploadDisposable.dispose()
|
||||||
|
}
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setup(composeOptions: ComposeActivity.ComposeOptions?) {
|
||||||
|
val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy
|
||||||
|
|
||||||
|
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
|
||||||
|
startingVisibility = Status.Visibility.byNum(
|
||||||
|
preferredVisibility.num.coerceAtLeast(replyVisibility.num))
|
||||||
|
statusVisibility.value = startingVisibility
|
||||||
|
|
||||||
|
inReplyToId = composeOptions?.inReplyToId
|
||||||
|
|
||||||
|
quoteId = composeOptions?.quoteId
|
||||||
|
quoteUrl = composeOptions?.quoteUrl
|
||||||
|
|
||||||
|
val contentWarning = composeOptions?.contentWarning
|
||||||
|
if (contentWarning != null) {
|
||||||
|
startingContentWarning = contentWarning
|
||||||
|
}
|
||||||
|
|
||||||
|
// recreate media list
|
||||||
|
// when coming from SavedTootActivity
|
||||||
|
val loadedDraftMediaUris = composeOptions?.mediaUrls
|
||||||
|
val loadedDraftMediaDescriptions: List<String?>? = composeOptions?.mediaDescriptions
|
||||||
|
if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) {
|
||||||
|
loadedDraftMediaUris.zip(loadedDraftMediaDescriptions)
|
||||||
|
.forEach { (uri, description) ->
|
||||||
|
pickMedia(uri.toUri()).observeForever { errorOrItem ->
|
||||||
|
if (errorOrItem.isRight() && description != null) {
|
||||||
|
updateDescription(errorOrItem.asRight().localId, description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else composeOptions?.mediaAttachments?.forEach { a ->
|
||||||
|
// when coming from redraft
|
||||||
|
val mediaType = when (a.type) {
|
||||||
|
Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO
|
||||||
|
Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE
|
||||||
|
else -> QueuedMedia.Type.IMAGE
|
||||||
|
}
|
||||||
|
addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
savedTootUid = composeOptions?.savedTootUid ?: 0
|
||||||
|
startingText = composeOptions?.tootText
|
||||||
|
|
||||||
|
|
||||||
|
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
|
||||||
|
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
|
||||||
|
startingVisibility = tootVisibility
|
||||||
|
}
|
||||||
|
val builder = StringBuilder()
|
||||||
|
val mentionedUsernames = composeOptions?.mentionedUsernames
|
||||||
|
if (mentionedUsernames != null) {
|
||||||
|
for (name in mentionedUsernames) {
|
||||||
|
builder.append('@')
|
||||||
|
builder.append(name)
|
||||||
|
builder.append(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (startingText != null) {
|
||||||
|
builder.append(startingText)
|
||||||
|
}
|
||||||
|
startingText = builder.toString()
|
||||||
|
|
||||||
|
|
||||||
|
scheduledAt.value = composeOptions?.scheduledAt
|
||||||
|
|
||||||
|
composeOptions?.sensitive?.let { markMediaAsSensitive.value = it }
|
||||||
|
|
||||||
|
val poll = composeOptions?.poll
|
||||||
|
if (poll != null && composeOptions.mediaAttachments.isNullOrEmpty()) {
|
||||||
|
this.poll.value = poll
|
||||||
|
}
|
||||||
|
replyingStatusContent = composeOptions?.replyingStatusContent
|
||||||
|
replyingStatusAuthor = composeOptions?.replyingStatusAuthor
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updatePoll(newPoll: NewPoll) {
|
||||||
|
poll.value = newPoll
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateScheduledAt(newScheduledAt: String?) {
|
||||||
|
scheduledAt.value = newScheduledAt
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val TAG = "ComposeViewModel"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = 25
|
||||||
|
|
||||||
|
private val CAN_USE_QUOTE_ID = arrayOf("odakyu.app", "biwakodon.com", "dtp-mstdn.jp", "nitiasa.com", "comm.cx", "fedibird.com")
|
||||||
|
|
||||||
|
data class ComposeInstanceParams(
|
||||||
|
val maxChars: Int,
|
||||||
|
val pollMaxOptions: Int,
|
||||||
|
val pollMaxLength: Int,
|
||||||
|
val supportsScheduled: Boolean
|
||||||
|
)
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.util;
|
package com.keylesspalace.tusky.components.compose;
|
||||||
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
@ -21,6 +21,8 @@ import android.graphics.BitmapFactory;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
|
|
||||||
|
import com.keylesspalace.tusky.util.IOUtils;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
@ -42,10 +44,10 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||||
private File tempFile;
|
private File tempFile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param sizeLimit the maximum number of bytes each image can take
|
* @param sizeLimit the maximum number of bytes each image can take
|
||||||
* @param contentResolver to resolve the specified images' URIs
|
* @param contentResolver to resolve the specified images' URIs
|
||||||
* @param tempFile the file where the result will be stored
|
* @param tempFile the file where the result will be stored
|
||||||
* @param listener to whom the results are given
|
* @param listener to whom the results are given
|
||||||
*/
|
*/
|
||||||
public DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, File tempFile, Listener listener) {
|
public DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, File tempFile, Listener listener) {
|
||||||
this.sizeLimit = sizeLimit;
|
this.sizeLimit = sizeLimit;
|
||||||
|
@ -56,6 +58,25 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Boolean doInBackground(Uri... uris) {
|
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) {
|
for (Uri uri : uris) {
|
||||||
InputStream inputStream;
|
InputStream inputStream;
|
||||||
try {
|
try {
|
||||||
|
@ -118,27 +139,16 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||||
reorientedBitmap.recycle();
|
reorientedBitmap.recycle();
|
||||||
scaledImageSize /= 2;
|
scaledImageSize /= 2;
|
||||||
} while (tempFile.length() > sizeLimit);
|
} while (tempFile.length() > sizeLimit);
|
||||||
|
|
||||||
if (isCancelled()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
protected void onPostExecute(Boolean successful) {
|
* Used to communicate the results of the task.
|
||||||
if (successful) {
|
*/
|
||||||
listener.onSuccess(tempFile);
|
|
||||||
} else {
|
|
||||||
listener.onFailure();
|
|
||||||
}
|
|
||||||
super.onPostExecute(successful);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Used to communicate the results of the task. */
|
|
||||||
public interface Listener {
|
public interface Listener {
|
||||||
void onSuccess(File file);
|
void onSuccess(File file);
|
||||||
|
|
||||||
void onFailure();
|
void onFailure();
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -0,0 +1,105 @@
|
||||||
|
/* Copyright 2019 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.Context
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.PopupMenu
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.recyclerview.widget.AsyncListDiffer
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.components.compose.view.ProgressImageView
|
||||||
|
|
||||||
|
class MediaPreviewAdapter(
|
||||||
|
context: Context,
|
||||||
|
private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit,
|
||||||
|
private val onRemove: (ComposeActivity.QueuedMedia) -> Unit
|
||||||
|
) : RecyclerView.Adapter<MediaPreviewAdapter.PreviewViewHolder>() {
|
||||||
|
|
||||||
|
fun submitList(list: List<ComposeActivity.QueuedMedia>) {
|
||||||
|
this.differ.submitList(list)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onMediaClick(position: Int, view: View) {
|
||||||
|
val item = differ.currentList[position]
|
||||||
|
val popup = PopupMenu(view.context, view)
|
||||||
|
val addCaptionId = 1
|
||||||
|
val removeId = 2
|
||||||
|
popup.menu.add(0, addCaptionId, 0, R.string.action_set_caption)
|
||||||
|
popup.menu.add(0, removeId, 0, R.string.action_remove)
|
||||||
|
popup.setOnMenuItemClickListener { menuItem ->
|
||||||
|
when (menuItem.itemId) {
|
||||||
|
addCaptionId -> onAddCaption(item)
|
||||||
|
removeId -> onRemove(item)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
popup.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val thumbnailViewSize =
|
||||||
|
context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
|
||||||
|
|
||||||
|
override fun getItemCount(): Int = differ.currentList.size
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PreviewViewHolder {
|
||||||
|
return PreviewViewHolder(ProgressImageView(parent.context))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: PreviewViewHolder, position: Int) {
|
||||||
|
val item = differ.currentList[position]
|
||||||
|
holder.progressImageView.setChecked(!item.description.isNullOrEmpty())
|
||||||
|
holder.progressImageView.setProgress(item.uploadPercent)
|
||||||
|
Glide.with(holder.itemView.context)
|
||||||
|
.load(item.uri)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.dontAnimate()
|
||||||
|
.into(holder.progressImageView)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val differ = AsyncListDiffer(this, object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() {
|
||||||
|
override fun areItemsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
||||||
|
return oldItem.localId == newItem.localId
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ComposeActivity.QueuedMedia, newItem: ComposeActivity.QueuedMedia): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
inner class PreviewViewHolder(val progressImageView: ProgressImageView)
|
||||||
|
: RecyclerView.ViewHolder(progressImageView) {
|
||||||
|
init {
|
||||||
|
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
|
||||||
|
val margin = itemView.context.resources
|
||||||
|
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
|
||||||
|
val marginBottom = itemView.context.resources
|
||||||
|
.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom)
|
||||||
|
layoutParams.setMargins(margin, 0, margin, marginBottom)
|
||||||
|
progressImageView.layoutParams = layoutParams
|
||||||
|
progressImageView.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||||
|
progressImageView.setOnClickListener {
|
||||||
|
onMediaClick(adapterPosition, progressImageView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,203 @@
|
||||||
|
/* Copyright 2019 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.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Environment
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||||
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import com.keylesspalace.tusky.network.ProgressRequestBody
|
||||||
|
import com.keylesspalace.tusky.util.*
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.Single
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.MultipartBody
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
sealed class UploadEvent {
|
||||||
|
data class ProgressEvent(val percentage: Int) : UploadEvent()
|
||||||
|
data class FinishedEvent(val attachment: Attachment) : UploadEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createNewImageFile(context: Context): File {
|
||||||
|
// Create an image file name
|
||||||
|
val randomId = randomAlphanumericString(12)
|
||||||
|
val imageFileName = "Tusky_${randomId}_"
|
||||||
|
val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
||||||
|
return File.createTempFile(
|
||||||
|
imageFileName, /* prefix */
|
||||||
|
".jpg", /* suffix */
|
||||||
|
storageDir /* directory */
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PreparedMedia(val type: QueuedMedia.Type, val uri: Uri, val size: Long)
|
||||||
|
|
||||||
|
interface MediaUploader {
|
||||||
|
fun prepareMedia(inUri: Uri): Single<PreparedMedia>
|
||||||
|
fun uploadMedia(media: QueuedMedia): Observable<UploadEvent>
|
||||||
|
}
|
||||||
|
|
||||||
|
class VideoSizeException : Exception()
|
||||||
|
class MediaTypeException : Exception()
|
||||||
|
class CouldNotOpenFileException : Exception()
|
||||||
|
|
||||||
|
class MediaUploaderImpl(
|
||||||
|
private val context: Context,
|
||||||
|
private val mastodonApi: MastodonApi
|
||||||
|
) : MediaUploader {
|
||||||
|
override fun uploadMedia(media: QueuedMedia): Observable<UploadEvent> {
|
||||||
|
return Observable
|
||||||
|
.fromCallable {
|
||||||
|
if (shouldResizeMedia(media)) {
|
||||||
|
downsize(media)
|
||||||
|
}
|
||||||
|
media
|
||||||
|
}
|
||||||
|
.switchMap { upload(it) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun prepareMedia(inUri: Uri): Single<PreparedMedia> {
|
||||||
|
return Single.fromCallable {
|
||||||
|
var mediaSize = getMediaSize(contentResolver, inUri)
|
||||||
|
var uri = inUri
|
||||||
|
val mimeType = contentResolver.getType(uri)
|
||||||
|
|
||||||
|
val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp")
|
||||||
|
|
||||||
|
try {
|
||||||
|
contentResolver.openInputStream(inUri).use { input ->
|
||||||
|
if (input == null) {
|
||||||
|
Log.w(TAG, "Media input is null")
|
||||||
|
uri = inUri
|
||||||
|
return@use
|
||||||
|
}
|
||||||
|
val file = File.createTempFile("randomTemp1", suffix, context.cacheDir)
|
||||||
|
FileOutputStream(file.absoluteFile).use { out ->
|
||||||
|
input.copyTo(out)
|
||||||
|
uri = FileProvider.getUriForFile(context,
|
||||||
|
BuildConfig.APPLICATION_ID + ".fileprovider",
|
||||||
|
file)
|
||||||
|
mediaSize = getMediaSize(contentResolver, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.w(TAG, e)
|
||||||
|
uri = inUri
|
||||||
|
}
|
||||||
|
if (mediaSize == MEDIA_SIZE_UNKNOWN) {
|
||||||
|
throw CouldNotOpenFileException()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mimeType != null) {
|
||||||
|
val topLevelType = mimeType.substring(0, mimeType.indexOf('/'))
|
||||||
|
when (topLevelType) {
|
||||||
|
"video" -> {
|
||||||
|
if (mediaSize > STATUS_VIDEO_SIZE_LIMIT) {
|
||||||
|
throw VideoSizeException()
|
||||||
|
}
|
||||||
|
PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize)
|
||||||
|
}
|
||||||
|
"image" -> {
|
||||||
|
PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
throw MediaTypeException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw MediaTypeException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val contentResolver = context.contentResolver
|
||||||
|
|
||||||
|
private fun upload(media: QueuedMedia): Observable<UploadEvent> {
|
||||||
|
return Observable.create { emitter ->
|
||||||
|
var mimeType = contentResolver.getType(media.uri)
|
||||||
|
val map = MimeTypeMap.getSingleton()
|
||||||
|
val fileExtension = map.getExtensionFromMimeType(mimeType)
|
||||||
|
val filename = String.format("%s_%s_%s.%s",
|
||||||
|
context.getString(R.string.app_name),
|
||||||
|
Date().time.toString(),
|
||||||
|
randomAlphanumericString(10),
|
||||||
|
fileExtension)
|
||||||
|
|
||||||
|
val stream = contentResolver.openInputStream(media.uri)
|
||||||
|
|
||||||
|
if (mimeType == null) mimeType = "multipart/form-data"
|
||||||
|
|
||||||
|
|
||||||
|
var lastProgress = -1
|
||||||
|
val fileBody = ProgressRequestBody(stream, media.mediaSize,
|
||||||
|
mimeType.toMediaTypeOrNull()) { percentage ->
|
||||||
|
if (percentage != lastProgress) {
|
||||||
|
emitter.onNext(UploadEvent.ProgressEvent(percentage))
|
||||||
|
}
|
||||||
|
lastProgress = percentage
|
||||||
|
}
|
||||||
|
|
||||||
|
val body = MultipartBody.Part.createFormData("file", filename, fileBody)
|
||||||
|
|
||||||
|
val uploadDisposable = mastodonApi.uploadMedia(body)
|
||||||
|
.subscribe({ attachment ->
|
||||||
|
emitter.onNext(UploadEvent.FinishedEvent(attachment))
|
||||||
|
emitter.onComplete()
|
||||||
|
}, { e ->
|
||||||
|
emitter.onError(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Cancel the request when our observable is cancelled
|
||||||
|
emitter.setDisposable(uploadDisposable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downsize(media: QueuedMedia): QueuedMedia {
|
||||||
|
val file = createNewImageFile(context)
|
||||||
|
DownsizeImageTask.resize(arrayOf(media.uri),
|
||||||
|
STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file)
|
||||||
|
return media.copy(uri = file.toUri(), mediaSize = file.length())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun shouldResizeMedia(media: QueuedMedia): Boolean {
|
||||||
|
return media.type == QueuedMedia.Type.IMAGE
|
||||||
|
&& (media.mediaSize > STATUS_IMAGE_SIZE_LIMIT
|
||||||
|
|| getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
private const val TAG = "MediaUploaderImpl"
|
||||||
|
private const val STATUS_VIDEO_SIZE_LIMIT = 41943040 // 40MiB
|
||||||
|
private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB
|
||||||
|
private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,29 +15,28 @@
|
||||||
|
|
||||||
@file:JvmName("AddPollDialog")
|
@file:JvmName("AddPollDialog")
|
||||||
|
|
||||||
package com.keylesspalace.tusky.view
|
package com.keylesspalace.tusky.components.compose.dialog
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.WindowManager
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import com.keylesspalace.tusky.ComposeActivity
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.adapter.AddPollOptionsAdapter
|
import com.keylesspalace.tusky.adapter.AddPollOptionsAdapter
|
||||||
import com.keylesspalace.tusky.entity.NewPoll
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
import kotlinx.android.synthetic.main.dialog_add_poll.view.*
|
import kotlinx.android.synthetic.main.dialog_add_poll.view.*
|
||||||
import android.view.WindowManager
|
|
||||||
import com.keylesspalace.tusky.R
|
|
||||||
|
|
||||||
private const val DEFAULT_MAX_OPTION_COUNT = 4
|
|
||||||
private const val DEFAULT_MAX_OPTION_LENGTH = 25
|
|
||||||
|
|
||||||
fun showAddPollDialog(
|
fun showAddPollDialog(
|
||||||
activity: ComposeActivity,
|
context: Context,
|
||||||
poll: NewPoll?,
|
poll: NewPoll?,
|
||||||
maxOptionCount: Int?,
|
maxOptionCount: Int,
|
||||||
maxOptionLength: Int?
|
maxOptionLength: Int,
|
||||||
|
onUpdatePoll: (NewPoll) -> Unit
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val view = activity.layoutInflater.inflate(R.layout.dialog_add_poll, null)
|
val view = LayoutInflater.from(context).inflate(R.layout.dialog_add_poll, null)
|
||||||
|
|
||||||
val dialog = AlertDialog.Builder(activity)
|
val dialog = AlertDialog.Builder(context)
|
||||||
.setIcon(R.drawable.ic_poll_24dp)
|
.setIcon(R.drawable.ic_poll_24dp)
|
||||||
.setTitle(R.string.create_poll_title)
|
.setTitle(R.string.create_poll_title)
|
||||||
.setView(view)
|
.setView(view)
|
||||||
|
@ -47,7 +46,7 @@ fun showAddPollDialog(
|
||||||
|
|
||||||
val adapter = AddPollOptionsAdapter(
|
val adapter = AddPollOptionsAdapter(
|
||||||
options = poll?.options?.toMutableList() ?: mutableListOf("", ""),
|
options = poll?.options?.toMutableList() ?: mutableListOf("", ""),
|
||||||
maxOptionLength = maxOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
|
maxOptionLength = maxOptionLength,
|
||||||
onOptionRemoved = { valid ->
|
onOptionRemoved = { valid ->
|
||||||
view.addChoiceButton.isEnabled = true
|
view.addChoiceButton.isEnabled = true
|
||||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid
|
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid
|
||||||
|
@ -60,15 +59,15 @@ fun showAddPollDialog(
|
||||||
view.pollChoices.adapter = adapter
|
view.pollChoices.adapter = adapter
|
||||||
|
|
||||||
view.addChoiceButton.setOnClickListener {
|
view.addChoiceButton.setOnClickListener {
|
||||||
if (adapter.itemCount < maxOptionCount ?: DEFAULT_MAX_OPTION_COUNT) {
|
if (adapter.itemCount < maxOptionCount) {
|
||||||
adapter.addChoice()
|
adapter.addChoice()
|
||||||
}
|
}
|
||||||
if (adapter.itemCount >= maxOptionCount ?: DEFAULT_MAX_OPTION_COUNT) {
|
if (adapter.itemCount >= maxOptionCount) {
|
||||||
it.isEnabled = false
|
it.isEnabled = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val pollDurationId = activity.resources.getIntArray(R.array.poll_duration_values).indexOfLast {
|
val pollDurationId = context.resources.getIntArray(R.array.poll_duration_values).indexOfLast {
|
||||||
it <= poll?.expiresIn ?: 0
|
it <= poll?.expiresIn ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,15 +80,14 @@ fun showAddPollDialog(
|
||||||
button.setOnClickListener {
|
button.setOnClickListener {
|
||||||
val selectedPollDurationId = view.pollDurationSpinner.selectedItemPosition
|
val selectedPollDurationId = view.pollDurationSpinner.selectedItemPosition
|
||||||
|
|
||||||
val pollDuration = activity.resources.getIntArray(R.array.poll_duration_values)[selectedPollDurationId]
|
val pollDuration = context.resources
|
||||||
|
.getIntArray(R.array.poll_duration_values)[selectedPollDurationId]
|
||||||
|
|
||||||
activity.updatePoll(
|
onUpdatePoll(NewPoll(
|
||||||
NewPoll(
|
options = adapter.pollOptions,
|
||||||
options = adapter.pollOptions,
|
expiresIn = pollDuration,
|
||||||
expiresIn = pollDuration,
|
multiple = view.multipleChoicesCheckBox.isChecked
|
||||||
multiple = view.multipleChoicesCheckBox.isChecked
|
))
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
/* Copyright 2019 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.dialog
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.net.Uri
|
||||||
|
import android.text.InputFilter
|
||||||
|
import android.text.InputType
|
||||||
|
import android.util.DisplayMetrics
|
||||||
|
import android.view.WindowManager
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.ImageView
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import at.connyduck.sparkbutton.helpers.Utils
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.request.target.CustomTarget
|
||||||
|
import com.bumptech.glide.request.transition.Transition
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.util.withLifecycleContext
|
||||||
|
|
||||||
|
// https://github.com/tootsuite/mastodon/blob/1656663/app/models/media_attachment.rb#L94
|
||||||
|
private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 420
|
||||||
|
|
||||||
|
|
||||||
|
fun <T> T.makeCaptionDialog(existingDescription: String?,
|
||||||
|
previewUri: Uri,
|
||||||
|
onUpdateDescription: (String) -> LiveData<Boolean>
|
||||||
|
) where T : Activity, T : LifecycleOwner {
|
||||||
|
val dialogLayout = LinearLayout(this)
|
||||||
|
val padding = Utils.dpToPx(this, 8)
|
||||||
|
dialogLayout.setPadding(padding, padding, padding, padding)
|
||||||
|
|
||||||
|
dialogLayout.orientation = LinearLayout.VERTICAL
|
||||||
|
val imageView = ImageView(this)
|
||||||
|
|
||||||
|
val displayMetrics = DisplayMetrics()
|
||||||
|
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||||
|
|
||||||
|
val margin = Utils.dpToPx(this, 4)
|
||||||
|
dialogLayout.addView(imageView)
|
||||||
|
(imageView.layoutParams as LinearLayout.LayoutParams).weight = 1f
|
||||||
|
imageView.layoutParams.height = 0
|
||||||
|
(imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0)
|
||||||
|
|
||||||
|
val input = EditText(this)
|
||||||
|
input.hint = getString(R.string.hint_describe_for_visually_impaired,
|
||||||
|
MEDIA_DESCRIPTION_CHARACTER_LIMIT)
|
||||||
|
dialogLayout.addView(input)
|
||||||
|
(input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin)
|
||||||
|
input.setLines(2)
|
||||||
|
input.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
||||||
|
input.setText(existingDescription)
|
||||||
|
input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT))
|
||||||
|
|
||||||
|
val okListener = { dialog: DialogInterface, _: Int ->
|
||||||
|
onUpdateDescription(input.text.toString())
|
||||||
|
withLifecycleContext {
|
||||||
|
onUpdateDescription(input.text.toString())
|
||||||
|
.observe { success -> if (!success) showFailedCaptionMessage() }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
val dialog = AlertDialog.Builder(this)
|
||||||
|
.setView(dialogLayout)
|
||||||
|
.setPositiveButton(android.R.string.ok, okListener)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.create()
|
||||||
|
|
||||||
|
val window = dialog.window
|
||||||
|
window?.setSoftInputMode(
|
||||||
|
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
|
||||||
|
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
// Load the image and manually set it into the ImageView because it doesn't have a fixed
|
||||||
|
// size. Maybe we should limit the size of CustomTarget
|
||||||
|
Glide.with(this)
|
||||||
|
.load(previewUri)
|
||||||
|
.into(object : CustomTarget<Drawable>() {
|
||||||
|
override fun onLoadCleared(placeholder: Drawable?) {}
|
||||||
|
|
||||||
|
override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) {
|
||||||
|
imageView.setImageDrawable(resource)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private fun Activity.showFailedCaptionMessage() {
|
||||||
|
Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.view
|
package com.keylesspalace.tusky.components.compose.view
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.view;
|
package com.keylesspalace.tusky.components.compose.view;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
@ -30,6 +30,7 @@ import com.google.android.material.datepicker.DateValidatorPointForward;
|
||||||
import com.google.android.material.datepicker.MaterialDatePicker;
|
import com.google.android.material.datepicker.MaterialDatePicker;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.fragment.TimePickerFragment;
|
import com.keylesspalace.tusky.fragment.TimePickerFragment;
|
||||||
|
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
import java.text.DateFormat;
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
|
@ -87,7 +88,7 @@ public class ComposeScheduleView extends ConstraintLayout {
|
||||||
|
|
||||||
private void setScheduledDateTime() {
|
private void setScheduledDateTime() {
|
||||||
if (scheduleDateTime == null) {
|
if (scheduleDateTime == null) {
|
||||||
scheduledDateTimeView.setText(R.string.hint_configure_scheduled_toot);
|
scheduledDateTimeView.setText("");
|
||||||
} else {
|
} else {
|
||||||
scheduledDateTimeView.setText(String.format("%s %s",
|
scheduledDateTimeView.setText(String.format("%s %s",
|
||||||
dateFormat.format(scheduleDateTime.getTime()),
|
dateFormat.format(scheduleDateTime.getTime()),
|
||||||
|
@ -96,13 +97,13 @@ public class ComposeScheduleView extends ConstraintLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setEditIcons() {
|
private void setEditIcons() {
|
||||||
final int size = scheduledDateTimeView.getLineHeight();
|
Drawable icon = ThemeUtils.getTintedDrawable(getContext(), R.drawable.ic_create_24dp, android.R.attr.textColorTertiary);
|
||||||
|
|
||||||
Drawable icon = getContext().getDrawable(R.drawable.ic_create_24dp);
|
|
||||||
if (icon == null) {
|
if (icon == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final int size = scheduledDateTimeView.getLineHeight();
|
||||||
|
|
||||||
icon.setBounds(0, 0, size, size);
|
icon.setBounds(0, 0, size, size);
|
||||||
|
|
||||||
scheduledDateTimeView.setCompoundDrawables(null, null, icon, null);
|
scheduledDateTimeView.setCompoundDrawables(null, null, icon, null);
|
||||||
|
@ -117,7 +118,7 @@ public class ComposeScheduleView extends ConstraintLayout {
|
||||||
setScheduledDateTime();
|
setScheduledDateTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openPickDateDialog() {
|
public void openPickDateDialog() {
|
||||||
long yesterday = Calendar.getInstance().getTimeInMillis() - 24 * 60 * 60 * 1000;
|
long yesterday = Calendar.getInstance().getTimeInMillis() - 24 * 60 * 60 * 1000;
|
||||||
CalendarConstraints calendarConstraints = new CalendarConstraints.Builder()
|
CalendarConstraints calendarConstraints = new CalendarConstraints.Builder()
|
||||||
.setValidator(
|
.setValidator(
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.view
|
package com.keylesspalace.tusky.components.compose.view
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.emoji.widget.EmojiEditTextHelper
|
import androidx.emoji.widget.EmojiEditTextHelper
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.view
|
package com.keylesspalace.tusky.components.compose.view
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.view;
|
package com.keylesspalace.tusky.components.compose.view;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.view
|
package com.keylesspalace.tusky.components.compose.view
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
|
@ -108,14 +108,11 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
||||||
|
|
||||||
setupButtons(listener, account.getId(), false, account.getUsername());
|
setupButtons(listener, account.getId(), false, account.getUsername());
|
||||||
|
|
||||||
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getEmojis(), listener, false);
|
setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getEmojis(), PollViewDataKt.toViewData(status.getPoll()), listener, false);
|
||||||
|
|
||||||
setConversationName(conversation.getAccounts());
|
setConversationName(conversation.getAccounts());
|
||||||
|
|
||||||
setAvatars(conversation.getAccounts());
|
setAvatars(conversation.getAccounts());
|
||||||
|
|
||||||
setupPoll(PollViewDataKt.toViewData(status.getPoll()), status.getEmojis(), listener);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setConversationName(List<ConversationAccountEntity> accounts) {
|
private void setConversationName(List<ConversationAccountEntity> accounts) {
|
||||||
|
|
|
@ -38,7 +38,12 @@ import androidx.paging.PagedListAdapter
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.keylesspalace.tusky.*
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
|
import com.keylesspalace.tusky.MainActivity
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.ViewMediaActivity
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions
|
||||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||||
import com.keylesspalace.tusky.components.search.adapter.SearchStatusesAdapter
|
import com.keylesspalace.tusky.components.search.adapter.SearchStatusesAdapter
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
|
@ -201,14 +206,14 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
|
||||||
mentionedUsernames.add(username)
|
mentionedUsernames.add(username)
|
||||||
}
|
}
|
||||||
mentionedUsernames.remove(loggedInUsername)
|
mentionedUsernames.remove(loggedInUsername)
|
||||||
val intent = ComposeActivity.IntentBuilder()
|
val intent = ComposeActivity.startIntent(context!!, ComposeOptions(
|
||||||
.inReplyToId(inReplyToId)
|
inReplyToId = inReplyToId,
|
||||||
.replyVisibility(replyVisibility)
|
replyVisibility = replyVisibility,
|
||||||
.contentWarning(contentWarning)
|
contentWarning = contentWarning,
|
||||||
.mentionedUsernames(mentionedUsernames)
|
mentionedUsernames = mentionedUsernames,
|
||||||
.replyingStatusAuthor(actionableStatus.account.localUsername)
|
replyingStatusAuthor = actionableStatus.account.localUsername,
|
||||||
.replyingStatusContent(actionableStatus.content.toString())
|
replyingStatusContent = actionableStatus.content.toString()
|
||||||
.build(context)
|
))
|
||||||
requireActivity().startActivity(intent)
|
requireActivity().startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,12 +230,12 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
|
||||||
mentionedUsernames.add(username)
|
mentionedUsernames.add(username)
|
||||||
}
|
}
|
||||||
mentionedUsernames.remove(loggedInUsername)
|
mentionedUsernames.remove(loggedInUsername)
|
||||||
val intent = ComposeActivity.IntentBuilder()
|
val intent = ComposeActivity.startIntent(context!!, ComposeOptions(
|
||||||
.quoteId(id)
|
quoteId = id,
|
||||||
.quoteUrl(url)
|
quoteUrl = url,
|
||||||
.replyVisibility(visibility)
|
replyVisibility = visibility,
|
||||||
.mentionedUsernames(mentionedUsernames)
|
mentionedUsernames = mentionedUsernames
|
||||||
.build(context)
|
))
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -426,24 +431,24 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
|
||||||
viewModel.deleteStatus(id)
|
viewModel.deleteStatus(id)
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||||
.subscribe ({ deletedStatus ->
|
.subscribe({ deletedStatus ->
|
||||||
removeItem(position)
|
removeItem(position)
|
||||||
|
|
||||||
val redraftStatus = if(deletedStatus.isEmpty()) {
|
val redraftStatus = if (deletedStatus.isEmpty()) {
|
||||||
status.toDeletedStatus()
|
status.toDeletedStatus()
|
||||||
} else {
|
} else {
|
||||||
deletedStatus
|
deletedStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
val intent = ComposeActivity.IntentBuilder()
|
val intent = ComposeActivity.startIntent(context!!, ComposeOptions(
|
||||||
.tootText(redraftStatus.text)
|
tootText = redraftStatus.text ?: "",
|
||||||
.inReplyToId(redraftStatus.inReplyToId)
|
inReplyToId = redraftStatus.inReplyToId,
|
||||||
.visibility(redraftStatus.visibility)
|
visibility = redraftStatus.visibility,
|
||||||
.contentWarning(redraftStatus.spoilerText)
|
contentWarning = redraftStatus.spoilerText,
|
||||||
.mediaAttachments(redraftStatus.attachments)
|
mediaAttachments = redraftStatus.attachments,
|
||||||
.sensitive(redraftStatus.sensitive)
|
sensitive = redraftStatus.sensitive,
|
||||||
.poll(redraftStatus.poll?.toNewPoll(status.createdAt))
|
poll = redraftStatus.poll?.toNewPoll(status.createdAt)
|
||||||
.build(context)
|
))
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}, { error ->
|
}, { error ->
|
||||||
Log.w("SearchStatusesFragment", "error deleting status", error)
|
Log.w("SearchStatusesFragment", "error deleting status", error)
|
||||||
|
|
|
@ -30,7 +30,7 @@ import androidx.annotation.NonNull;
|
||||||
|
|
||||||
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||||
TimelineAccountEntity.class, ConversationEntity.class
|
TimelineAccountEntity.class, ConversationEntity.class
|
||||||
}, version = 20)
|
}, version = 21)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public abstract TootDao tootDao();
|
public abstract TootDao tootDao();
|
||||||
|
@ -316,6 +316,14 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `bookmarked` INTEGER NOT NULL DEFAULT 0");
|
database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `bookmarked` INTEGER NOT NULL DEFAULT 0");
|
||||||
database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_bookmarked` INTEGER NOT NULL DEFAULT 0");
|
database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_bookmarked` INTEGER NOT NULL DEFAULT 0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_20_21 = new Migration(20, 21) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `version` TEXT");
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
|
@ -19,6 +19,7 @@ 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.Single
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface InstanceDao {
|
interface InstanceDao {
|
||||||
|
@ -26,5 +27,5 @@ interface InstanceDao {
|
||||||
fun insertOrReplace(instance: InstanceEntity)
|
fun insertOrReplace(instance: InstanceEntity)
|
||||||
|
|
||||||
@Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1")
|
@Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1")
|
||||||
fun loadMetadataForInstance(instance: String): InstanceEntity?
|
fun loadMetadataForInstance(instance: String): Single<InstanceEntity>
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,5 +27,6 @@ data class InstanceEntity(
|
||||||
val emojiList: List<Emoji>?,
|
val emojiList: List<Emoji>?,
|
||||||
val maximumTootCharacters: Int?,
|
val maximumTootCharacters: Int?,
|
||||||
val maxPollOptions: Int?,
|
val maxPollOptions: Int?,
|
||||||
val maxPollOptionLength: Int?
|
val maxPollOptionLength: Int?,
|
||||||
|
val version: String?
|
||||||
)
|
)
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
package com.keylesspalace.tusky.di
|
package com.keylesspalace.tusky.di
|
||||||
|
|
||||||
import com.keylesspalace.tusky.*
|
import com.keylesspalace.tusky.*
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||||
import com.keylesspalace.tusky.components.search.SearchActivity
|
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||||
|
|
|
@ -35,7 +35,8 @@ import javax.inject.Singleton
|
||||||
ServicesModule::class,
|
ServicesModule::class,
|
||||||
BroadcastReceiverModule::class,
|
BroadcastReceiverModule::class,
|
||||||
ViewModelModule::class,
|
ViewModelModule::class,
|
||||||
RepositoryModule::class
|
RepositoryModule::class,
|
||||||
|
MediaUploaderModule::class
|
||||||
])
|
])
|
||||||
interface AppComponent {
|
interface AppComponent {
|
||||||
@Component.Builder
|
@Component.Builder
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
/* Copyright 2019 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.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import com.keylesspalace.tusky.components.compose.MediaUploader
|
||||||
|
import com.keylesspalace.tusky.components.compose.MediaUploaderImpl
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
|
||||||
|
@Module
|
||||||
|
class MediaUploaderModule {
|
||||||
|
@Provides
|
||||||
|
fun providesMediaUploder(context: Context, mastodonApi: MastodonApi): MediaUploader =
|
||||||
|
MediaUploaderImpl(context, mastodonApi)
|
||||||
|
}
|
|
@ -15,12 +15,25 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.di
|
package com.keylesspalace.tusky.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import com.keylesspalace.tusky.service.SendTootService
|
import com.keylesspalace.tusky.service.SendTootService
|
||||||
|
import com.keylesspalace.tusky.service.ServiceClient
|
||||||
|
import com.keylesspalace.tusky.service.ServiceClientImpl
|
||||||
import dagger.Module
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
import dagger.android.ContributesAndroidInjector
|
import dagger.android.ContributesAndroidInjector
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
abstract class ServicesModule {
|
abstract class ServicesModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesSendTootService(): SendTootService
|
abstract fun contributesSendTootService(): SendTootService
|
||||||
|
|
||||||
|
@Module
|
||||||
|
companion object {
|
||||||
|
@Provides
|
||||||
|
@JvmStatic
|
||||||
|
fun providesServiceClient(context: Context): ServiceClient {
|
||||||
|
return ServiceClientImpl(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -4,10 +4,13 @@ package com.keylesspalace.tusky.di
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
||||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||||
import com.keylesspalace.tusky.components.search.SearchViewModel
|
import com.keylesspalace.tusky.components.search.SearchViewModel
|
||||||
import com.keylesspalace.tusky.viewmodel.*
|
import com.keylesspalace.tusky.viewmodel.AccountViewModel
|
||||||
|
import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel
|
||||||
|
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||||
import com.keylesspalace.tusky.viewmodel.ListsViewModel
|
import com.keylesspalace.tusky.viewmodel.ListsViewModel
|
||||||
import dagger.Binds
|
import dagger.Binds
|
||||||
import dagger.MapKey
|
import dagger.MapKey
|
||||||
|
@ -71,5 +74,10 @@ abstract class ViewModelModule {
|
||||||
@ViewModelKey(SearchViewModel::class)
|
@ViewModelKey(SearchViewModel::class)
|
||||||
internal abstract fun searchViewModel(viewModel: SearchViewModel): ViewModel
|
internal abstract fun searchViewModel(viewModel: SearchViewModel): ViewModel
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(ComposeViewModel::class)
|
||||||
|
internal abstract fun composeViewModel(viewModel: ComposeViewModel): ViewModel
|
||||||
|
|
||||||
//Add more ViewModels here
|
//Add more ViewModels here
|
||||||
}
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
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
|
||||||
|
)
|
|
@ -42,12 +42,13 @@ import androidx.lifecycle.Lifecycle;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.BaseActivity;
|
import com.keylesspalace.tusky.BaseActivity;
|
||||||
import com.keylesspalace.tusky.BottomSheetActivity;
|
import com.keylesspalace.tusky.BottomSheetActivity;
|
||||||
import com.keylesspalace.tusky.ComposeActivity;
|
|
||||||
import com.keylesspalace.tusky.MainActivity;
|
import com.keylesspalace.tusky.MainActivity;
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.PostLookupFallbackBehavior;
|
import com.keylesspalace.tusky.PostLookupFallbackBehavior;
|
||||||
import com.keylesspalace.tusky.ViewMediaActivity;
|
import com.keylesspalace.tusky.ViewMediaActivity;
|
||||||
import com.keylesspalace.tusky.ViewTagActivity;
|
import com.keylesspalace.tusky.ViewTagActivity;
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions;
|
||||||
import com.keylesspalace.tusky.components.report.ReportActivity;
|
import com.keylesspalace.tusky.components.report.ReportActivity;
|
||||||
import com.keylesspalace.tusky.db.AccountEntity;
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
|
@ -148,21 +149,22 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
||||||
mentionedUsernames.add(actionableStatus.getAccount().getUsername());
|
mentionedUsernames.add(actionableStatus.getAccount().getUsername());
|
||||||
String loggedInUsername = null;
|
String loggedInUsername = null;
|
||||||
AccountEntity activeAccount = accountManager.getActiveAccount();
|
AccountEntity activeAccount = accountManager.getActiveAccount();
|
||||||
if(activeAccount != null) {
|
if (activeAccount != null) {
|
||||||
loggedInUsername = activeAccount.getUsername();
|
loggedInUsername = activeAccount.getUsername();
|
||||||
}
|
}
|
||||||
for (Status.Mention mention : mentions) {
|
for (Status.Mention mention : mentions) {
|
||||||
mentionedUsernames.add(mention.getUsername());
|
mentionedUsernames.add(mention.getUsername());
|
||||||
}
|
}
|
||||||
mentionedUsernames.remove(loggedInUsername);
|
mentionedUsernames.remove(loggedInUsername);
|
||||||
Intent intent = new ComposeActivity.IntentBuilder()
|
ComposeOptions composeOptions = new ComposeOptions();
|
||||||
.inReplyToId(inReplyToId)
|
composeOptions.setInReplyToId(inReplyToId);
|
||||||
.replyVisibility(replyVisibility)
|
composeOptions.setReplyVisibility(replyVisibility);
|
||||||
.contentWarning(contentWarning)
|
composeOptions.setContentWarning(contentWarning);
|
||||||
.mentionedUsernames(mentionedUsernames)
|
composeOptions.setMentionedUsernames(mentionedUsernames);
|
||||||
.replyingStatusAuthor(actionableStatus.getAccount().getLocalUsername())
|
composeOptions.setReplyingStatusAuthor(actionableStatus.getAccount().getLocalUsername());
|
||||||
.replyingStatusContent(actionableStatus.getContent().toString())
|
composeOptions.setReplyingStatusContent(actionableStatus.getContent().toString());
|
||||||
.build(getContext());
|
|
||||||
|
Intent intent = ComposeActivity.startIntent(getContext(), composeOptions);
|
||||||
getActivity().startActivity(intent);
|
getActivity().startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,12 +188,13 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
||||||
if (status.getReblog() != null) {
|
if (status.getReblog() != null) {
|
||||||
url = status.getReblog().getUrl();
|
url = status.getReblog().getUrl();
|
||||||
}
|
}
|
||||||
Intent intent = new ComposeActivity.IntentBuilder()
|
ComposeOptions composeOptions = new ComposeOptions();
|
||||||
.quoteId(id)
|
composeOptions.setQuoteId(id);
|
||||||
.quoteUrl(url)
|
composeOptions.setQuoteUrl(url);
|
||||||
.replyVisibility(visibility)
|
composeOptions.setReplyVisibility(visibility);
|
||||||
.mentionedUsernames(mentionedUsernames)
|
composeOptions.setMentionedUsernames(mentionedUsernames);
|
||||||
.build(getContext());
|
|
||||||
|
Intent intent = ComposeActivity.startIntent(getContext(), composeOptions);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -205,7 +208,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
||||||
|
|
||||||
String loggedInAccountId = null;
|
String loggedInAccountId = null;
|
||||||
AccountEntity activeAccount = accountManager.getActiveAccount();
|
AccountEntity activeAccount = accountManager.getActiveAccount();
|
||||||
if(activeAccount != null) {
|
if (activeAccount != null) {
|
||||||
loggedInAccountId = activeAccount.getAccountId();
|
loggedInAccountId = activeAccount.getAccountId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,7 +241,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
||||||
|
|
||||||
Menu menu = popup.getMenu();
|
Menu menu = popup.getMenu();
|
||||||
MenuItem openAsItem = menu.findItem(R.id.status_open_as);
|
MenuItem openAsItem = menu.findItem(R.id.status_open_as);
|
||||||
switch(accounts.size()) {
|
switch (accounts.size()) {
|
||||||
case 0:
|
case 0:
|
||||||
case 1:
|
case 1:
|
||||||
openAsItem.setVisible(false);
|
openAsItem.setVisible(false);
|
||||||
|
@ -261,7 +264,8 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.status_share_content: {
|
case R.id.status_share_content: {
|
||||||
Status statusToShare = status;
|
Status statusToShare = status;
|
||||||
if(statusToShare.getReblog() != null) statusToShare = statusToShare.getReblog();
|
if (statusToShare.getReblog() != null)
|
||||||
|
statusToShare = statusToShare.getReblog();
|
||||||
|
|
||||||
Intent sendIntent = new Intent();
|
Intent sendIntent = new Intent();
|
||||||
sendIntent.setAction(Intent.ACTION_SEND);
|
sendIntent.setAction(Intent.ACTION_SEND);
|
||||||
|
@ -386,7 +390,8 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
||||||
.subscribe(
|
.subscribe(
|
||||||
deletedStatus -> {},
|
deletedStatus -> {
|
||||||
|
},
|
||||||
error -> {
|
error -> {
|
||||||
Log.w("SFragment", "error deleting status", error);
|
Log.w("SFragment", "error deleting status", error);
|
||||||
Toast.makeText(getContext(), R.string.error_generic, Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), R.string.error_generic, Toast.LENGTH_SHORT).show();
|
||||||
|
@ -410,22 +415,22 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
||||||
.subscribe(deletedStatus -> {
|
.subscribe(deletedStatus -> {
|
||||||
removeItem(position);
|
removeItem(position);
|
||||||
|
|
||||||
if(deletedStatus.isEmpty()) {
|
if (deletedStatus.isEmpty()) {
|
||||||
deletedStatus = status.toDeletedStatus();
|
deletedStatus = status.toDeletedStatus();
|
||||||
}
|
}
|
||||||
|
ComposeOptions composeOptions = new ComposeOptions();
|
||||||
ComposeActivity.IntentBuilder intentBuilder = new ComposeActivity.IntentBuilder()
|
composeOptions.setTootText(deletedStatus.getText());
|
||||||
.tootText(deletedStatus.getText())
|
composeOptions.setInReplyToId(deletedStatus.getInReplyToId());
|
||||||
.inReplyToId(deletedStatus.getInReplyToId())
|
composeOptions.setVisibility(deletedStatus.getVisibility());
|
||||||
.visibility(deletedStatus.getVisibility())
|
composeOptions.setContentWarning(deletedStatus.getSpoilerText());
|
||||||
.contentWarning(deletedStatus.getSpoilerText())
|
composeOptions.setMediaAttachments(deletedStatus.getAttachments());
|
||||||
.mediaAttachments(deletedStatus.getAttachments())
|
composeOptions.setSensitive(deletedStatus.getSensitive());
|
||||||
.sensitive(deletedStatus.getSensitive());
|
if (deletedStatus.getPoll() != null) {
|
||||||
if(deletedStatus.getPoll() != null) {
|
composeOptions.setPoll(deletedStatus.getPoll().toNewPoll(deletedStatus.getCreatedAt()));
|
||||||
intentBuilder.poll(deletedStatus.getPoll().toNewPoll(deletedStatus.getCreatedAt()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Intent intent = intentBuilder.build(getContext());
|
Intent intent = ComposeActivity
|
||||||
|
.startIntent(getContext(), composeOptions);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
},
|
},
|
||||||
error -> {
|
error -> {
|
||||||
|
@ -444,22 +449,22 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||||
intent.putExtra(MainActivity.STATUS_URL, statusUrl);
|
intent.putExtra(MainActivity.STATUS_URL, statusUrl);
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
((BaseActivity)getActivity()).finishWithoutSlideOutAnimation();
|
((BaseActivity) getActivity()).finishWithoutSlideOutAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showOpenAsDialog(String statusUrl, CharSequence dialogTitle) {
|
private void showOpenAsDialog(String statusUrl, CharSequence dialogTitle) {
|
||||||
BaseActivity activity = (BaseActivity)getActivity();
|
BaseActivity activity = (BaseActivity) getActivity();
|
||||||
activity.showAccountChooserDialog(dialogTitle, false, account -> openAsAccount(statusUrl, account));
|
activity.showAccountChooserDialog(dialogTitle, false, account -> openAsAccount(statusUrl, account));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void downloadAllMedia(Status status) {
|
private void downloadAllMedia(Status status) {
|
||||||
Toast.makeText(getContext(), R.string.downloading_media, Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), R.string.downloading_media, Toast.LENGTH_SHORT).show();
|
||||||
for(Attachment attachment: status.getAttachments()) {
|
for (Attachment attachment : status.getAttachments()) {
|
||||||
String url = attachment.getUrl();
|
String url = attachment.getUrl();
|
||||||
Uri uri = Uri.parse(url);
|
Uri uri = Uri.parse(url);
|
||||||
String filename = uri.getLastPathSegment();
|
String filename = uri.getLastPathSegment();
|
||||||
|
|
||||||
DownloadManager downloadManager = (DownloadManager)getActivity().getSystemService(Context.DOWNLOAD_SERVICE);
|
DownloadManager downloadManager = (DownloadManager) getActivity().getSystemService(Context.DOWNLOAD_SERVICE);
|
||||||
DownloadManager.Request request = new DownloadManager.Request(uri);
|
DownloadManager.Request request = new DownloadManager.Request(uri);
|
||||||
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename);
|
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename);
|
||||||
downloadManager.enqueue(request);
|
downloadManager.enqueue(request);
|
||||||
|
@ -467,8 +472,8 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void requestDownloadAllMedia(Status status) {
|
private void requestDownloadAllMedia(Status status) {
|
||||||
String[] permissions = new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE };
|
String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
|
||||||
((BaseActivity)getActivity()).requestPermissions(permissions, (permissions1, grantResults) -> {
|
((BaseActivity) getActivity()).requestPermissions(permissions, (permissions1, grantResults) -> {
|
||||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
downloadAllMedia(status);
|
downloadAllMedia(status);
|
||||||
} else {
|
} else {
|
||||||
|
@ -516,9 +521,9 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public boolean shouldFilterStatus(Status status) {
|
public boolean shouldFilterStatus(Status status) {
|
||||||
|
|
||||||
if(filterRemoveRegex && status.getPoll() != null) {
|
if (filterRemoveRegex && status.getPoll() != null) {
|
||||||
for(PollOption option: status.getPoll().getOptions()) {
|
for (PollOption option : status.getPoll().getOptions()) {
|
||||||
if(filterRemoveRegexMatcher.reset(option.getTitle()).find()) {
|
if (filterRemoveRegexMatcher.reset(option.getTitle()).find()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.DialogFragment;
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.ComposeActivity;
|
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||||
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.TimeZone;
|
import java.util.TimeZone;
|
||||||
|
|
|
@ -18,9 +18,10 @@ package com.keylesspalace.tusky.fragment.preference
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
import androidx.preference.PreferenceFragmentCompat
|
||||||
import com.keylesspalace.tusky.ComposeActivity
|
|
||||||
import com.keylesspalace.tusky.PreferencesActivity
|
import com.keylesspalace.tusky.PreferencesActivity
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions
|
||||||
import com.keylesspalace.tusky.util.ThemeUtils
|
import com.keylesspalace.tusky.util.ThemeUtils
|
||||||
import com.keylesspalace.tusky.util.getNonNullString
|
import com.keylesspalace.tusky.util.getNonNullString
|
||||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||||
|
@ -120,10 +121,10 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
||||||
val sendCrashReportPreference = requirePreference("sendCrashReport")
|
val sendCrashReportPreference = requirePreference("sendCrashReport")
|
||||||
sendCrashReportPreference.setOnPreferenceClickListener {
|
sendCrashReportPreference.setOnPreferenceClickListener {
|
||||||
activity?.let { activity ->
|
activity?.let { activity ->
|
||||||
val intent = ComposeActivity.IntentBuilder()
|
val intent = ComposeActivity.startIntent(activity, ComposeOptions(
|
||||||
.tootText("@ars42525@odakyu.app $stackTrace".substring(0, 400))
|
tootText = "@ars42525@odakyu.app $stackTrace".substring(0, 400),
|
||||||
.contentWarning("Yuito StackTrace")
|
contentWarning = "Yuito StackTrace"
|
||||||
.build(activity)
|
))
|
||||||
activity.startActivity(intent)
|
activity.startActivity(intent)
|
||||||
sharedPreferences.edit()
|
sharedPreferences.edit()
|
||||||
.remove("stack_trace")
|
.remove("stack_trace")
|
||||||
|
|
|
@ -43,7 +43,7 @@ interface MastodonApi {
|
||||||
fun getLists(): Single<List<MastoList>>
|
fun getLists(): Single<List<MastoList>>
|
||||||
|
|
||||||
@GET("/api/v1/custom_emojis")
|
@GET("/api/v1/custom_emojis")
|
||||||
fun getCustomEmojis(): Call<List<Emoji>>
|
fun getCustomEmojis(): Single<List<Emoji>>
|
||||||
|
|
||||||
@GET("api/v1/instance")
|
@GET("api/v1/instance")
|
||||||
fun getInstance(): Single<Instance>
|
fun getInstance(): Single<Instance>
|
||||||
|
@ -116,14 +116,14 @@ interface MastodonApi {
|
||||||
@POST("api/v1/media")
|
@POST("api/v1/media")
|
||||||
fun uploadMedia(
|
fun uploadMedia(
|
||||||
@Part file: MultipartBody.Part
|
@Part file: MultipartBody.Part
|
||||||
): Call<Attachment>
|
): Single<Attachment>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@PUT("api/v1/media/{mediaId}")
|
@PUT("api/v1/media/{mediaId}")
|
||||||
fun updateMedia(
|
fun updateMedia(
|
||||||
@Path("mediaId") mediaId: String,
|
@Path("mediaId") mediaId: String,
|
||||||
@Field("description") description: String
|
@Field("description") description: String
|
||||||
): Call<Attachment>
|
): Single<Attachment>
|
||||||
|
|
||||||
@POST("api/v1/statuses")
|
@POST("api/v1/statuses")
|
||||||
fun createStatus(
|
fun createStatus(
|
||||||
|
@ -238,10 +238,10 @@ interface MastodonApi {
|
||||||
|
|
||||||
@GET("api/v1/accounts/search")
|
@GET("api/v1/accounts/search")
|
||||||
fun searchAccounts(
|
fun searchAccounts(
|
||||||
@Query("q") q: String,
|
@Query("q") query: String,
|
||||||
@Query("resolve") resolve: Boolean?,
|
@Query("resolve") resolve: Boolean? = null,
|
||||||
@Query("limit") limit: Int?,
|
@Query("limit") limit: Int? = null,
|
||||||
@Query("following") following: Boolean?
|
@Query("following") following: Boolean? = null
|
||||||
): Single<List<Account>>
|
): Single<List<Account>>
|
||||||
|
|
||||||
@GET("api/v1/accounts/{id}")
|
@GET("api/v1/accounts/{id}")
|
||||||
|
@ -318,6 +318,11 @@ interface MastodonApi {
|
||||||
@Query("id[]") accountIds: List<String>
|
@Query("id[]") accountIds: List<String>
|
||||||
): Call<List<Relationship>>
|
): Call<List<Relationship>>
|
||||||
|
|
||||||
|
@GET("api/v1/accounts/{id}/identity_proofs")
|
||||||
|
fun identityProofs(
|
||||||
|
@Path("id") accountId: String
|
||||||
|
): Call<List<IdentityProof>>
|
||||||
|
|
||||||
@GET("api/v1/blocks")
|
@GET("api/v1/blocks")
|
||||||
fun blocks(
|
fun blocks(
|
||||||
@Query("max_id") maxId: String?
|
@Query("max_id") maxId: String?
|
||||||
|
|
|
@ -23,12 +23,15 @@ import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.app.RemoteInput
|
import androidx.core.app.RemoteInput
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.keylesspalace.tusky.ComposeActivity
|
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.service.SendTootService
|
import com.keylesspalace.tusky.service.SendTootService
|
||||||
|
import com.keylesspalace.tusky.service.TootToSend
|
||||||
import com.keylesspalace.tusky.util.NotificationHelper
|
import com.keylesspalace.tusky.util.NotificationHelper
|
||||||
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
import dagger.android.AndroidInjection
|
import dagger.android.AndroidInjection
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@ -85,19 +88,25 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
val sendIntent = SendTootService.sendTootIntent(
|
val sendIntent = SendTootService.sendTootIntent(
|
||||||
context,
|
context,
|
||||||
text,
|
TootToSend(
|
||||||
spoiler,
|
text,
|
||||||
visibility,
|
spoiler,
|
||||||
false,
|
visibility.serverString(),
|
||||||
emptyList(),
|
false,
|
||||||
emptyList(),
|
emptyList(),
|
||||||
emptyList(),
|
emptyList(),
|
||||||
null,
|
emptyList(),
|
||||||
citedStatusId,
|
null,
|
||||||
null,
|
citedStatusId,
|
||||||
null,
|
null,
|
||||||
null,
|
null,
|
||||||
null, null, account, 0)
|
null,
|
||||||
|
null, null, account.id,
|
||||||
|
0,
|
||||||
|
randomAlphanumericString(16),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
context.startService(sendIntent)
|
context.startService(sendIntent)
|
||||||
|
|
||||||
|
@ -125,14 +134,14 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
accountManager.setActiveAccount(senderId)
|
accountManager.setActiveAccount(senderId)
|
||||||
|
|
||||||
val composeIntent = ComposeActivity.IntentBuilder()
|
val composeIntent = ComposeActivity.startIntent(context, ComposeOptions(
|
||||||
.inReplyToId(citedStatusId)
|
inReplyToId = citedStatusId,
|
||||||
.replyVisibility(visibility)
|
replyVisibility = visibility,
|
||||||
.contentWarning(spoiler)
|
contentWarning = spoiler,
|
||||||
.mentionedUsernames(mentions.toList())
|
mentionedUsernames = mentions.toSet(),
|
||||||
.replyingStatusAuthor(localAuthorId)
|
replyingStatusAuthor = localAuthorId,
|
||||||
.replyingStatusContent(citedText)
|
replyingStatusContent = citedText
|
||||||
.build(context)
|
))
|
||||||
|
|
||||||
composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ import android.content.ClipData
|
||||||
import android.content.ClipDescription
|
import android.content.ClipDescription
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
@ -19,7 +18,6 @@ import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
||||||
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
|
||||||
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.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
@ -28,7 +26,6 @@ import com.keylesspalace.tusky.entity.NewStatus
|
||||||
import com.keylesspalace.tusky.entity.Status
|
import com.keylesspalace.tusky.entity.Status
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.SaveTootHelper
|
import com.keylesspalace.tusky.util.SaveTootHelper
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
|
||||||
import dagger.android.AndroidInjection
|
import dagger.android.AndroidInjection
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
|
@ -50,7 +47,8 @@ class SendTootService : Service(), Injectable {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var database: AppDatabase
|
lateinit var database: AppDatabase
|
||||||
|
|
||||||
private lateinit var saveTootHelper: SaveTootHelper
|
@Inject
|
||||||
|
lateinit var saveTootHelper: SaveTootHelper
|
||||||
|
|
||||||
private val tootsToSend = ConcurrentHashMap<Int, TootToSend>()
|
private val tootsToSend = ConcurrentHashMap<Int, TootToSend>()
|
||||||
private val sendCalls = ConcurrentHashMap<Int, Call<Status>>()
|
private val sendCalls = ConcurrentHashMap<Int, Call<Status>>()
|
||||||
|
@ -61,7 +59,6 @@ class SendTootService : Service(), Injectable {
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
AndroidInjection.inject(this)
|
AndroidInjection.inject(this)
|
||||||
saveTootHelper = SaveTootHelper(database.tootDao(), this)
|
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -285,56 +282,19 @@ class SendTootService : Service(), Injectable {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun sendTootIntent(context: Context,
|
fun sendTootIntent(context: Context,
|
||||||
text: String,
|
tootToSend: TootToSend
|
||||||
warningText: String,
|
|
||||||
visibility: Status.Visibility,
|
|
||||||
sensitive: Boolean,
|
|
||||||
mediaIds: List<String>,
|
|
||||||
mediaUris: List<Uri>,
|
|
||||||
mediaDescriptions: List<String>,
|
|
||||||
scheduledAt: String?,
|
|
||||||
inReplyToId: String?,
|
|
||||||
poll: NewPoll?,
|
|
||||||
replyingStatusContent: String?,
|
|
||||||
replyingStatusAuthorUsername: String?,
|
|
||||||
savedJsonUrls: String?,
|
|
||||||
quoteId: String?,
|
|
||||||
account: AccountEntity,
|
|
||||||
savedTootUid: Int
|
|
||||||
): Intent {
|
): Intent {
|
||||||
val intent = Intent(context, SendTootService::class.java)
|
val intent = Intent(context, SendTootService::class.java)
|
||||||
|
|
||||||
val idempotencyKey = randomAlphanumericString(16)
|
|
||||||
|
|
||||||
val tootToSend = TootToSend(text,
|
|
||||||
warningText,
|
|
||||||
visibility.serverString(),
|
|
||||||
sensitive,
|
|
||||||
mediaIds,
|
|
||||||
mediaUris.map { it.toString() },
|
|
||||||
mediaDescriptions,
|
|
||||||
scheduledAt,
|
|
||||||
inReplyToId,
|
|
||||||
poll,
|
|
||||||
replyingStatusContent,
|
|
||||||
replyingStatusAuthorUsername,
|
|
||||||
savedJsonUrls,
|
|
||||||
quoteId,
|
|
||||||
account.id,
|
|
||||||
savedTootUid,
|
|
||||||
idempotencyKey,
|
|
||||||
0)
|
|
||||||
|
|
||||||
intent.putExtra(KEY_TOOT, tootToSend)
|
intent.putExtra(KEY_TOOT, tootToSend)
|
||||||
|
|
||||||
if(mediaUris.isNotEmpty()) {
|
if (tootToSend.mediaUris.isNotEmpty()) {
|
||||||
// forward uri permissions
|
// forward uri permissions
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
val uriClip = ClipData(
|
val uriClip = ClipData(
|
||||||
ClipDescription("Toot Media", arrayOf("image/*", "video/*")),
|
ClipDescription("Toot Media", arrayOf("image/*", "video/*")),
|
||||||
ClipData.Item(mediaUris[0])
|
ClipData.Item(tootToSend.mediaUris[0])
|
||||||
)
|
)
|
||||||
mediaUris
|
tootToSend.mediaUris
|
||||||
.drop(1)
|
.drop(1)
|
||||||
.forEach { mediaUri ->
|
.forEach { mediaUri ->
|
||||||
uriClip.addItem(ClipData.Item(mediaUri))
|
uriClip.addItem(ClipData.Item(mediaUri))
|
||||||
|
@ -351,21 +311,23 @@ class SendTootService : Service(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class TootToSend(val text: String,
|
data class TootToSend(
|
||||||
val warningText: String,
|
val text: String,
|
||||||
val visibility: String,
|
val warningText: String,
|
||||||
val sensitive: Boolean,
|
val visibility: String,
|
||||||
val mediaIds: List<String>,
|
val sensitive: Boolean,
|
||||||
val mediaUris: List<String>,
|
val mediaIds: List<String>,
|
||||||
val mediaDescriptions: List<String>,
|
val mediaUris: List<String>,
|
||||||
val scheduledAt: String?,
|
val mediaDescriptions: List<String>,
|
||||||
val inReplyToId: String?,
|
val scheduledAt: String?,
|
||||||
val poll: NewPoll?,
|
val inReplyToId: String?,
|
||||||
val replyingStatusContent: String?,
|
val poll: NewPoll?,
|
||||||
val replyingStatusAuthorUsername: String?,
|
val replyingStatusContent: String?,
|
||||||
val savedJsonUrls: String?,
|
val replyingStatusAuthorUsername: String?,
|
||||||
val quoteId: String?,
|
val savedJsonUrls: List<String>?,
|
||||||
val accountId: Long,
|
val quoteId: String?,
|
||||||
val savedTootUid: Int,
|
val accountId: Long,
|
||||||
val idempotencyKey: String,
|
val savedTootUid: Int,
|
||||||
var retries: Int) : Parcelable
|
val idempotencyKey: String,
|
||||||
|
var retries: Int
|
||||||
|
) : Parcelable
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
/* Copyright 2019 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.service
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
|
||||||
|
interface ServiceClient {
|
||||||
|
fun sendToot(tootToSend: TootToSend)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServiceClientImpl(private val context: Context) : ServiceClient {
|
||||||
|
override fun sendToot(tootToSend: TootToSend) {
|
||||||
|
val intent = SendTootService.sendTootIntent(context, tootToSend)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
context.startForegroundService(intent)
|
||||||
|
} else {
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,6 @@ package com.keylesspalace.tusky.service
|
||||||
import android.annotation.TargetApi
|
import android.annotation.TargetApi
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.service.quicksettings.TileService
|
import android.service.quicksettings.TileService
|
||||||
|
|
||||||
import com.keylesspalace.tusky.MainActivity
|
import com.keylesspalace.tusky.MainActivity
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,51 +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.util;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a synchronization primitive related to {@link java.util.concurrent.CountDownLatch}
|
|
||||||
* except that it starts at zero and can count upward.
|
|
||||||
* <p>
|
|
||||||
* The intended use case is for waiting for all tasks to be finished when the number of tasks isn't
|
|
||||||
* known ahead of time, or may change while waiting.
|
|
||||||
*/
|
|
||||||
public class CountUpDownLatch {
|
|
||||||
private int count;
|
|
||||||
|
|
||||||
public CountUpDownLatch() {
|
|
||||||
this.count = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void countDown() {
|
|
||||||
count--;
|
|
||||||
notifyAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void countUp() {
|
|
||||||
count++;
|
|
||||||
notifyAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized void await() throws InterruptedException {
|
|
||||||
while (count != 0) {
|
|
||||||
wait();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public synchronized boolean isEmpty() {
|
|
||||||
return count == 0;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,6 +19,7 @@ import android.content.ActivityNotFoundException;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
|
@ -68,8 +69,8 @@ public class LinkHelper {
|
||||||
* @param listener to notify about particular spans that are clicked
|
* @param listener to notify about particular spans that are clicked
|
||||||
*/
|
*/
|
||||||
public static void setClickableText(TextView view, Spanned content,
|
public static void setClickableText(TextView view, Spanned content,
|
||||||
@Nullable Status.Mention[] mentions, final LinkListener listener,
|
@Nullable Status.Mention[] mentions, final LinkListener listener,
|
||||||
boolean removeQuote) {
|
boolean removeQuote) {
|
||||||
SpannableStringBuilder builder = new SpannableStringBuilder(content);
|
SpannableStringBuilder builder = new SpannableStringBuilder(content);
|
||||||
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class);
|
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class);
|
||||||
for (URLSpan span : urlSpans) {
|
for (URLSpan span : urlSpans) {
|
||||||
|
@ -186,6 +187,14 @@ public class LinkHelper {
|
||||||
view.setMovementMethod(LinkMovementMethod.getInstance());
|
view.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static CharSequence createClickableText(String text, String link) {
|
||||||
|
URLSpan span = new CustomURLSpan(link);
|
||||||
|
|
||||||
|
SpannableStringBuilder clickableText = new SpannableStringBuilder(text);
|
||||||
|
clickableText.setSpan(span, 0, text.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||||
|
return clickableText;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens a link, depending on the settings, either in the browser or in a custom tab
|
* Opens a link, depending on the settings, either in the browser or in a custom tab
|
||||||
*
|
*
|
||||||
|
@ -229,10 +238,17 @@ public class LinkHelper {
|
||||||
public static void openLinkInCustomTab(Uri uri, Context context) {
|
public static void openLinkInCustomTab(Uri uri, Context context) {
|
||||||
int toolbarColor = ThemeUtils.getColor(context, R.attr.custom_tab_toolbar);
|
int toolbarColor = ThemeUtils.getColor(context, R.attr.custom_tab_toolbar);
|
||||||
|
|
||||||
CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder()
|
CustomTabsIntent.Builder customTabsIntentBuilder = new CustomTabsIntent.Builder()
|
||||||
.setToolbarColor(toolbarColor)
|
.setToolbarColor(toolbarColor)
|
||||||
.setShowTitle(true)
|
.setShowTitle(true);
|
||||||
.build();
|
|
||||||
|
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
|
customTabsIntentBuilder.setNavigationBarColor(
|
||||||
|
ThemeUtils.getColor(context, android.R.attr.navigationBarColor)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomTabsIntent customTabsIntent = customTabsIntentBuilder.build();
|
||||||
try {
|
try {
|
||||||
customTabsIntent.launchUrl(context, uri);
|
customTabsIntent.launchUrl(context, uri);
|
||||||
} catch (ActivityNotFoundException e) {
|
} catch (ActivityNotFoundException e) {
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
/* Copyright 2019 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 androidx.lifecycle.*
|
||||||
|
import io.reactivex.BackpressureStrategy
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.Single
|
||||||
|
|
||||||
|
inline fun <X, Y> LiveData<X>.map(crossinline mapFunction: (X) -> Y): LiveData<Y> =
|
||||||
|
Transformations.map(this) { input -> mapFunction(input) }
|
||||||
|
|
||||||
|
inline fun <X, Y> LiveData<X>.switchMap(
|
||||||
|
crossinline switchMapFunction: (X) -> LiveData<Y>
|
||||||
|
): LiveData<Y> = Transformations.switchMap(this) { input -> switchMapFunction(input) }
|
||||||
|
|
||||||
|
inline fun <X> LiveData<X>.filter(crossinline predicate: (X) -> Boolean): LiveData<X> {
|
||||||
|
val liveData = MediatorLiveData<X>()
|
||||||
|
liveData.addSource(this) { value ->
|
||||||
|
if (predicate(value)) {
|
||||||
|
liveData.value = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return liveData
|
||||||
|
}
|
||||||
|
|
||||||
|
fun LifecycleOwner.withLifecycleContext(body: LifecycleContext.() -> Unit) =
|
||||||
|
LifecycleContext(this).apply(body)
|
||||||
|
|
||||||
|
class LifecycleContext(val lifecycleOwner: LifecycleOwner) {
|
||||||
|
inline fun <T> LiveData<T>.observe(crossinline observer: (T) -> Unit) =
|
||||||
|
this.observe(lifecycleOwner, Observer { observer(it) })
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Just hold a subscription,
|
||||||
|
*/
|
||||||
|
fun <T> LiveData<T>.subscribe() =
|
||||||
|
this.observe(lifecycleOwner, Observer { })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invokes @param [combiner] when value of both @param [a] and @param [b] are not null. Returns
|
||||||
|
* [LiveData] with value set to the result of calling [combiner] with value of both.
|
||||||
|
* Important! You still need to observe to the returned [LiveData] for [combiner] to be invoked.
|
||||||
|
*/
|
||||||
|
fun <A, B, R> combineLiveData(a: LiveData<A>, b: LiveData<B>, combiner: (A, B) -> R): LiveData<R> {
|
||||||
|
val liveData = MediatorLiveData<R>()
|
||||||
|
liveData.addSource(a) {
|
||||||
|
if (a.value != null && b.value != null) {
|
||||||
|
liveData.value = combiner(a.value!!, b.value!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
liveData.addSource(b) {
|
||||||
|
if (a.value != null && b.value != null) {
|
||||||
|
liveData.value = combiner(a.value!!, b.value!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return liveData
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns [LiveData] with value set to the result of calling [combiner] with value of [a] and [b]
|
||||||
|
* after either changes. Doesn't check if either has value.
|
||||||
|
* Important! You still need to observe to the returned [LiveData] for [combiner] to be invoked.
|
||||||
|
*/
|
||||||
|
fun <A, B, R> combineOptionalLiveData(a: LiveData<A>, b: LiveData<B>, combiner: (A?, B?) -> R): LiveData<R> {
|
||||||
|
val liveData = MediatorLiveData<R>()
|
||||||
|
liveData.addSource(a) {
|
||||||
|
liveData.value = combiner(a.value, b.value)
|
||||||
|
}
|
||||||
|
liveData.addSource(b) {
|
||||||
|
liveData.value = combiner(a.value, b.value)
|
||||||
|
}
|
||||||
|
return liveData
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Single<T>.toLiveData() = LiveDataReactiveStreams.fromPublisher(this.toFlowable())
|
||||||
|
fun <T> Observable<T>.toLiveData(
|
||||||
|
backpressureStrategy: BackpressureStrategy = BackpressureStrategy.LATEST
|
||||||
|
) = LiveDataReactiveStreams.fromPublisher(this.toFlowable(BackpressureStrategy.LATEST))
|
|
@ -16,13 +16,10 @@
|
||||||
package com.keylesspalace.tusky.util
|
package com.keylesspalace.tusky.util
|
||||||
|
|
||||||
import android.content.ContentResolver
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.Matrix
|
import android.graphics.Matrix
|
||||||
import android.media.MediaMetadataRetriever
|
|
||||||
import android.media.ThumbnailUtils
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import androidx.annotation.Px
|
import androidx.annotation.Px
|
||||||
|
@ -106,26 +103,6 @@ fun getSampledBitmap(contentResolver: ContentResolver, uri: Uri, @Px reqWidth: I
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getImageThumbnail(contentResolver: ContentResolver, uri: Uri, @Px thumbnailSize: Int): Bitmap? {
|
|
||||||
val source = getSampledBitmap(contentResolver, uri, thumbnailSize, thumbnailSize) ?: return null
|
|
||||||
return ThumbnailUtils.extractThumbnail(source, thumbnailSize, thumbnailSize, ThumbnailUtils.OPTIONS_RECYCLE_INPUT)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getVideoThumbnail(context: Context, uri: Uri, @Px thumbnailSize: Int): Bitmap? {
|
|
||||||
val retriever = MediaMetadataRetriever()
|
|
||||||
try {
|
|
||||||
retriever.setDataSource(context, uri)
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
Log.w(TAG, e)
|
|
||||||
return null
|
|
||||||
} catch (e: SecurityException) {
|
|
||||||
Log.w(TAG, e)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val source = retriever.frameAtTime ?: return null
|
|
||||||
return ThumbnailUtils.extractThumbnail(source, thumbnailSize, thumbnailSize, ThumbnailUtils.OPTIONS_RECYCLE_INPUT)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Throws(FileNotFoundException::class)
|
@Throws(FileNotFoundException::class)
|
||||||
fun getImageSquarePixels(contentResolver: ContentResolver, uri: Uri): Long {
|
fun getImageSquarePixels(contentResolver: ContentResolver, uri: Uri): Long {
|
||||||
val input = contentResolver.openInputStream(uri)
|
val input = contentResolver.openInputStream(uri)
|
||||||
|
|
|
@ -5,16 +5,18 @@ import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.content.FileProvider;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.webkit.MimeTypeMap;
|
import android.webkit.MimeTypeMap;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.content.FileProvider;
|
||||||
|
|
||||||
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.BuildConfig;
|
import com.keylesspalace.tusky.BuildConfig;
|
||||||
|
import com.keylesspalace.tusky.db.AppDatabase;
|
||||||
import com.keylesspalace.tusky.db.TootDao;
|
import com.keylesspalace.tusky.db.TootDao;
|
||||||
import com.keylesspalace.tusky.db.TootEntity;
|
import com.keylesspalace.tusky.db.TootEntity;
|
||||||
import com.keylesspalace.tusky.entity.NewPoll;
|
import com.keylesspalace.tusky.entity.NewPoll;
|
||||||
|
@ -27,6 +29,8 @@ import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
public final class SaveTootHelper {
|
public final class SaveTootHelper {
|
||||||
|
|
||||||
private static final String TAG = "SaveTootHelper";
|
private static final String TAG = "SaveTootHelper";
|
||||||
|
@ -35,15 +39,16 @@ public final class SaveTootHelper {
|
||||||
private Context context;
|
private Context context;
|
||||||
private Gson gson = new Gson();
|
private Gson gson = new Gson();
|
||||||
|
|
||||||
public SaveTootHelper(@NonNull TootDao tootDao, @NonNull Context context) {
|
@Inject
|
||||||
this.tootDao = tootDao;
|
public SaveTootHelper(@NonNull AppDatabase appDatabase, @NonNull Context context) {
|
||||||
|
this.tootDao = appDatabase.tootDao();
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
public boolean saveToot(@NonNull String content,
|
public boolean saveToot(@NonNull String content,
|
||||||
@NonNull String contentWarning,
|
@NonNull String contentWarning,
|
||||||
@Nullable String savedJsonUrls,
|
@Nullable List<String> savedJsonUrls,
|
||||||
@NonNull List<String> mediaUris,
|
@NonNull List<String> mediaUris,
|
||||||
@NonNull List<String> mediaDescriptions,
|
@NonNull List<String> mediaDescriptions,
|
||||||
int savedTootUid,
|
int savedTootUid,
|
||||||
|
@ -58,31 +63,25 @@ public final class SaveTootHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get any existing file's URIs.
|
// Get any existing file's URIs.
|
||||||
ArrayList<String> existingUris = null;
|
|
||||||
if (!TextUtils.isEmpty(savedJsonUrls)) {
|
|
||||||
existingUris = gson.fromJson(savedJsonUrls,
|
|
||||||
new TypeToken<ArrayList<String>>() {
|
|
||||||
}.getType());
|
|
||||||
}
|
|
||||||
|
|
||||||
String mediaUrlsSerialized = null;
|
String mediaUrlsSerialized = null;
|
||||||
String mediaDescriptionsSerialized = null;
|
String mediaDescriptionsSerialized = null;
|
||||||
|
|
||||||
if (!ListUtils.isEmpty(mediaUris)) {
|
if (!ListUtils.isEmpty(mediaUris)) {
|
||||||
List<String> savedList = saveMedia(mediaUris, existingUris);
|
List<String> savedList = saveMedia(mediaUris, savedJsonUrls);
|
||||||
if (!ListUtils.isEmpty(savedList)) {
|
if (!ListUtils.isEmpty(savedList)) {
|
||||||
mediaUrlsSerialized = gson.toJson(savedList);
|
mediaUrlsSerialized = gson.toJson(savedList);
|
||||||
if (!ListUtils.isEmpty(existingUris)) {
|
if (!ListUtils.isEmpty(savedJsonUrls)) {
|
||||||
deleteMedia(setDifference(existingUris, savedList));
|
deleteMedia(setDifference(savedJsonUrls, savedList));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
mediaDescriptionsSerialized = gson.toJson(mediaDescriptions);
|
mediaDescriptionsSerialized = gson.toJson(mediaDescriptions);
|
||||||
} else if (!ListUtils.isEmpty(existingUris)) {
|
} else if (!ListUtils.isEmpty(savedJsonUrls)) {
|
||||||
/* If there were URIs in the previous draft, but they've now been removed, those files
|
/* If there were URIs in the previous draft, but they've now been removed, those files
|
||||||
* can be deleted. */
|
* can be deleted. */
|
||||||
deleteMedia(existingUris);
|
deleteMedia(savedJsonUrls);
|
||||||
}
|
}
|
||||||
final TootEntity toot = new TootEntity(savedTootUid, content, mediaUrlsSerialized, mediaDescriptionsSerialized, contentWarning,
|
final TootEntity toot = new TootEntity(savedTootUid, content, mediaUrlsSerialized, mediaDescriptionsSerialized, contentWarning,
|
||||||
inReplyToId,
|
inReplyToId,
|
||||||
|
@ -103,15 +102,16 @@ public final class SaveTootHelper {
|
||||||
|
|
||||||
public void deleteDraft(int tootId) {
|
public void deleteDraft(int tootId) {
|
||||||
TootEntity item = tootDao.find(tootId);
|
TootEntity item = tootDao.find(tootId);
|
||||||
if(item != null) {
|
if (item != null) {
|
||||||
deleteDraft(item);
|
deleteDraft(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteDraft(@NonNull TootEntity item){
|
public void deleteDraft(@NonNull TootEntity item) {
|
||||||
// Delete any media files associated with the status.
|
// Delete any media files associated with the status.
|
||||||
ArrayList<String> uris = gson.fromJson(item.getUrls(),
|
ArrayList<String> uris = gson.fromJson(item.getUrls(),
|
||||||
new TypeToken<ArrayList<String>>() {}.getType());
|
new TypeToken<ArrayList<String>>() {
|
||||||
|
}.getType());
|
||||||
if (uris != null) {
|
if (uris != null) {
|
||||||
for (String uriString : uris) {
|
for (String uriString : uris) {
|
||||||
Uri uri = Uri.parse(uriString);
|
Uri uri = Uri.parse(uriString);
|
||||||
|
@ -172,7 +172,7 @@ public final class SaveTootHelper {
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
Uri resultUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID+".fileprovider", file);
|
Uri resultUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file);
|
||||||
results.add(resultUri.toString());
|
results.add(resultUri.toString());
|
||||||
}
|
}
|
||||||
return results;
|
return results;
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.util;
|
package com.keylesspalace.tusky.util;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@ -24,7 +26,7 @@ public class VersionUtils {
|
||||||
private int minor;
|
private int minor;
|
||||||
private int patch;
|
private int patch;
|
||||||
|
|
||||||
public VersionUtils(String versionString) {
|
public VersionUtils(@NonNull String versionString) {
|
||||||
String regex = "([0-9]+)\\.([0-9]+)\\.([0-9]+).*";
|
String regex = "([0-9]+)\\.([0-9]+)\\.([0-9]+).*";
|
||||||
Pattern pattern = Pattern.compile(regex);
|
Pattern pattern = Pattern.compile(regex);
|
||||||
Matcher matcher = pattern.matcher(versionString);
|
Matcher matcher = pattern.matcher(versionString);
|
||||||
|
|
|
@ -51,4 +51,13 @@ inline fun EditText.onTextChanged(
|
||||||
callback(s, start, before, count)
|
callback(s, start, before, count)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fun EditText.afterTextChanged(
|
||||||
|
crossinline callback: (s: Editable) -> Unit) {
|
||||||
|
addTextChangedListener(object : DefaultTextWatcher() {
|
||||||
|
override fun afterTextChanged(s: Editable) {
|
||||||
|
callback(s)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
|
@ -6,12 +6,11 @@ import androidx.lifecycle.ViewModel
|
||||||
import com.keylesspalace.tusky.appstore.*
|
import com.keylesspalace.tusky.appstore.*
|
||||||
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.Error
|
import com.keylesspalace.tusky.util.*
|
||||||
import com.keylesspalace.tusky.util.Loading
|
|
||||||
import com.keylesspalace.tusky.util.Resource
|
|
||||||
import com.keylesspalace.tusky.util.Success
|
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.disposables.Disposable
|
||||||
import retrofit2.Call
|
import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
|
@ -27,6 +26,14 @@ class AccountViewModel @Inject constructor(
|
||||||
val accountData = MutableLiveData<Resource<Account>>()
|
val accountData = MutableLiveData<Resource<Account>>()
|
||||||
val relationshipData = MutableLiveData<Resource<Relationship>>()
|
val relationshipData = MutableLiveData<Resource<Relationship>>()
|
||||||
|
|
||||||
|
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<IdentityProof, Field>(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private val callList: MutableList<Call<*>> = mutableListOf()
|
private val callList: MutableList<Call<*>> = mutableListOf()
|
||||||
private val disposable: Disposable = eventHub.events
|
private val disposable: Disposable = eventHub.events
|
||||||
.subscribe { event ->
|
.subscribe { event ->
|
||||||
|
@ -60,6 +67,7 @@ class AccountViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call<Account>, t: Throwable) {
|
override fun onFailure(call: Call<Account>, t: Throwable) {
|
||||||
|
Log.w(TAG, "failed obtaining account", t)
|
||||||
accountData.postValue(Error())
|
accountData.postValue(Error())
|
||||||
isDataLoading = false
|
isDataLoading = false
|
||||||
isRefreshing.postValue(false)
|
isRefreshing.postValue(false)
|
||||||
|
@ -90,6 +98,7 @@ class AccountViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFailure(call: Call<List<Relationship>>, t: Throwable) {
|
override fun onFailure(call: Call<List<Relationship>>, t: Throwable) {
|
||||||
|
Log.w(TAG, "failed obtaining relationships", t)
|
||||||
relationshipData.postValue(Error())
|
relationshipData.postValue(Error())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -98,6 +107,30 @@ class AccountViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun obtainIdentityProof(reload: Boolean = false) {
|
||||||
|
if (identityProofData.value == null || reload) {
|
||||||
|
|
||||||
|
val call = mastodonApi.identityProofs(accountId)
|
||||||
|
call.enqueue(object : Callback<List<IdentityProof>> {
|
||||||
|
override fun onResponse(call: Call<List<IdentityProof>>,
|
||||||
|
response: Response<List<IdentityProof>>) {
|
||||||
|
val proofs = response.body()
|
||||||
|
if (response.isSuccessful && proofs != null ) {
|
||||||
|
identityProofData.postValue(proofs)
|
||||||
|
} else {
|
||||||
|
identityProofData.postValue(emptyList())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(call: Call<List<IdentityProof>>, t: Throwable) {
|
||||||
|
Log.w(TAG, "failed obtaining identity proofs", t)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
callList.add(call)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
@ -227,6 +260,7 @@ class AccountViewModel @Inject constructor(
|
||||||
return
|
return
|
||||||
accountId.let {
|
accountId.let {
|
||||||
obtainAccount(isReload)
|
obtainAccount(isReload)
|
||||||
|
obtainIdentityProof()
|
||||||
if (!isSelf)
|
if (!isSelf)
|
||||||
obtainRelationship(isReload)
|
obtainRelationship(isReload)
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,13 +12,13 @@ import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
|
|
||||||
import com.keylesspalace.tusky.ComposeActivity;
|
|
||||||
import com.keylesspalace.tusky.R;
|
import com.keylesspalace.tusky.R;
|
||||||
import com.keylesspalace.tusky.appstore.DrawerFooterClickedEvent;
|
import com.keylesspalace.tusky.appstore.DrawerFooterClickedEvent;
|
||||||
import com.keylesspalace.tusky.appstore.Event;
|
import com.keylesspalace.tusky.appstore.Event;
|
||||||
import com.keylesspalace.tusky.appstore.EventHub;
|
import com.keylesspalace.tusky.appstore.EventHub;
|
||||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent;
|
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent;
|
||||||
import com.keylesspalace.tusky.appstore.QuickReplyEvent;
|
import com.keylesspalace.tusky.appstore.QuickReplyEvent;
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||||
import com.keylesspalace.tusky.db.AccountEntity;
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
@ -28,8 +28,9 @@ import java.util.Arrays;
|
||||||
import java.util.LinkedHashSet;
|
import java.util.LinkedHashSet;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import static com.keylesspalace.tusky.ComposeActivity.PREF_DEFAULT_TAG;
|
import static com.keylesspalace.tusky.components.compose.ComposeActivity.CAN_USE_UNLEAKABLE;
|
||||||
import static com.keylesspalace.tusky.ComposeActivity.PREF_USE_DEFAULT_TAG;
|
import static com.keylesspalace.tusky.components.compose.ComposeActivity.PREF_DEFAULT_TAG;
|
||||||
|
import static com.keylesspalace.tusky.components.compose.ComposeActivity.PREF_USE_DEFAULT_TAG;
|
||||||
|
|
||||||
public class QuickTootHelper {
|
public class QuickTootHelper {
|
||||||
|
|
||||||
|
@ -74,8 +75,7 @@ public class QuickTootHelper {
|
||||||
|
|
||||||
public void composeButton() {
|
public void composeButton() {
|
||||||
if (tootEditText.getText().length() == 0 && inReplyTo == null) {
|
if (tootEditText.getText().length() == 0 && inReplyTo == null) {
|
||||||
Intent composeIntent = new Intent(context, ComposeActivity.class);
|
context.startActivity(getComposeIntent(context, true, false));
|
||||||
context.startActivity(composeIntent);
|
|
||||||
} else {
|
} else {
|
||||||
startComposeWithQuickComposeData();
|
startComposeWithQuickComposeData();
|
||||||
}
|
}
|
||||||
|
@ -107,43 +107,45 @@ public class QuickTootHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startComposeWithQuickComposeData() {
|
private void startComposeWithQuickComposeData() {
|
||||||
Intent composeIntent = setupIntentBuilder(false);
|
Intent intent = getComposeIntent(context, false, false);
|
||||||
resetQuickCompose();
|
resetQuickCompose();
|
||||||
context.startActivity(composeIntent);
|
context.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void quickToot() {
|
private void quickToot() {
|
||||||
if (tootEditText.getText().toString().length() > 0) {
|
if (tootEditText.getText().toString().length() > 0) {
|
||||||
Intent composeIntent = setupIntentBuilder(true);
|
Intent intent = getComposeIntent(context, false, true);
|
||||||
resetQuickCompose();
|
resetQuickCompose();
|
||||||
context.startActivity(composeIntent);
|
context.startActivity(intent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Intent setupIntentBuilder(boolean tootRightNow) {
|
private Intent getComposeIntent(Context context, boolean onlyVisibility, boolean tootRightNow) {
|
||||||
ComposeActivity.IntentBuilder intentBuilder = new ComposeActivity.IntentBuilder()
|
ComposeActivity.ComposeOptions options = new ComposeActivity.ComposeOptions();
|
||||||
.tootText(tootEditText.getText().toString())
|
options.setVisibility(getCurrentVisibility());
|
||||||
.visibility(getCurrentVisibility())
|
if (onlyVisibility) {
|
||||||
.tootRightNow(tootRightNow);
|
return ComposeActivity.startIntent(context, options);
|
||||||
|
}
|
||||||
|
options.setTootText(tootEditText.getText().toString());
|
||||||
|
options.setTootRightNow(tootRightNow);
|
||||||
|
|
||||||
if (inReplyTo == null) {
|
if (inReplyTo != null) {
|
||||||
return intentBuilder.build(context);
|
Status.Mention[] mentions = inReplyTo.getMentions();
|
||||||
|
Set<String> mentionedUsernames = new LinkedHashSet<>();
|
||||||
|
mentionedUsernames.add(inReplyTo.getAccount().getUsername());
|
||||||
|
for (Status.Mention mention : mentions) {
|
||||||
|
mentionedUsernames.add(mention.getUsername());
|
||||||
|
}
|
||||||
|
mentionedUsernames.remove(loggedInUsername);
|
||||||
|
|
||||||
|
options.setInReplyToId(inReplyTo.getId());
|
||||||
|
options.setContentWarning(inReplyTo.getSpoilerText());
|
||||||
|
options.setMentionedUsernames(mentionedUsernames);
|
||||||
|
options.setReplyingStatusAuthor(inReplyTo.getAccount().getLocalUsername());
|
||||||
|
options.setReplyingStatusContent(inReplyTo.getContent().toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
Status.Mention[] mentions = inReplyTo.getMentions();
|
return ComposeActivity.startIntent(context, options);
|
||||||
Set<String> mentionedUsernames = new LinkedHashSet<>();
|
|
||||||
mentionedUsernames.add(inReplyTo.getAccount().getUsername());
|
|
||||||
for (Status.Mention mention : mentions) {
|
|
||||||
mentionedUsernames.add(mention.getUsername());
|
|
||||||
}
|
|
||||||
mentionedUsernames.remove(loggedInUsername);
|
|
||||||
|
|
||||||
return intentBuilder.inReplyToId(inReplyTo.getId())
|
|
||||||
.contentWarning(inReplyTo.getSpoilerText())
|
|
||||||
.mentionedUsernames(mentionedUsernames)
|
|
||||||
.replyingStatusAuthor(inReplyTo.getAccount().getLocalUsername())
|
|
||||||
.replyingStatusContent(inReplyTo.getContent().toString())
|
|
||||||
.build(context);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void resetQuickCompose() {
|
private void resetQuickCompose() {
|
||||||
|
@ -178,7 +180,7 @@ public class QuickTootHelper {
|
||||||
|
|
||||||
private Status.Visibility getCurrentVisibility() {
|
private Status.Visibility getCurrentVisibility() {
|
||||||
Status.Visibility visibility = Status.Visibility.byNum(defPrefs.getInt(PREF_CURRENT_VISIBILITY, Status.Visibility.PUBLIC.getNum()));
|
Status.Visibility visibility = Status.Visibility.byNum(defPrefs.getInt(PREF_CURRENT_VISIBILITY, Status.Visibility.PUBLIC.getNum()));
|
||||||
if (!Arrays.asList(ComposeActivity.CAN_USE_UNLEAKABLE)
|
if (!Arrays.asList(CAN_USE_UNLEAKABLE)
|
||||||
.contains(domain) && visibility == Status.Visibility.UNLEAKABLE) {
|
.contains(domain) && visibility == Status.Visibility.UNLEAKABLE) {
|
||||||
defPrefs.edit()
|
defPrefs.edit()
|
||||||
.putInt(PREF_CURRENT_VISIBILITY, Status.Visibility.PUBLIC.getNum())
|
.putInt(PREF_CURRENT_VISIBILITY, Status.Visibility.PUBLIC.getNum())
|
||||||
|
@ -217,8 +219,7 @@ public class QuickTootHelper {
|
||||||
visibility = Status.Visibility.PRIVATE;
|
visibility = Status.Visibility.PRIVATE;
|
||||||
break;
|
break;
|
||||||
case PRIVATE:
|
case PRIVATE:
|
||||||
if (Arrays.asList(ComposeActivity.CAN_USE_UNLEAKABLE)
|
if (Arrays.asList(CAN_USE_UNLEAKABLE).contains(domain)) {
|
||||||
.contains(domain)) {
|
|
||||||
visibility = Status.Visibility.UNLEAKABLE;
|
visibility = Status.Visibility.UNLEAKABLE;
|
||||||
} else {
|
} else {
|
||||||
visibility = Status.Visibility.PUBLIC;
|
visibility = Status.Visibility.PUBLIC;
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/activity_compose"
|
android:id="@+id/activityCompose"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
@ -30,10 +30,9 @@
|
||||||
android:layout_gravity="end"
|
android:layout_gravity="end"
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:text="@string/at_symbol"
|
android:text="@string/at_symbol"
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="?android:textColorTertiary"
|
android:textColor="?android:textColorTertiary"
|
||||||
android:textSize="?attr/status_text_large"
|
android:textSize="?attr/status_text_large"
|
||||||
/>
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatButton
|
<androidx.appcompat.widget.AppCompatButton
|
||||||
android:id="@+id/hashButton"
|
android:id="@+id/hashButton"
|
||||||
|
@ -43,10 +42,9 @@
|
||||||
android:layout_gravity="end"
|
android:layout_gravity="end"
|
||||||
android:padding="8dp"
|
android:padding="8dp"
|
||||||
android:text="@string/hash_symbol"
|
android:text="@string/hash_symbol"
|
||||||
android:textStyle="bold"
|
|
||||||
android:textColor="?android:textColorTertiary"
|
android:textColor="?android:textColorTertiary"
|
||||||
android:textSize="?attr/status_text_large"
|
android:textSize="?attr/status_text_large"
|
||||||
/>
|
android:textStyle="bold" />
|
||||||
</androidx.appcompat.widget.Toolbar>
|
</androidx.appcompat.widget.Toolbar>
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
<androidx.core.widget.NestedScrollView
|
||||||
|
@ -131,7 +129,7 @@
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<com.keylesspalace.tusky.view.EditTextTyped
|
<com.keylesspalace.tusky.components.compose.view.EditTextTyped
|
||||||
android:id="@+id/composeEditField"
|
android:id="@+id/composeEditField"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -148,25 +146,19 @@
|
||||||
android:textColorHint="?android:attr/textColorTertiary"
|
android:textColorHint="?android:attr/textColorTertiary"
|
||||||
android:textSize="?attr/status_text_large" />
|
android:textSize="?attr/status_text_large" />
|
||||||
|
|
||||||
<HorizontalScrollView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/composeMediaPreviewBar"
|
||||||
|
android:visibility="gone"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:scrollbars="none">
|
android:scrollbars="none" />
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/compose_media_preview_bar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:paddingLeft="16dp"
|
|
||||||
android:paddingRight="16dp">
|
|
||||||
|
|
||||||
<!--This is filled at runtime with ImageView's for each preview in the upload queue.-->
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</HorizontalScrollView>
|
|
||||||
|
|
||||||
|
<com.keylesspalace.tusky.components.compose.view.PollPreviewView
|
||||||
|
android:id="@+id/pollPreview"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
@ -178,7 +170,7 @@
|
||||||
android:paddingBottom="52dp" >
|
android:paddingBottom="52dp" >
|
||||||
|
|
||||||
<CheckBox
|
<CheckBox
|
||||||
android:id="@+id/checkbox_use_default_text"
|
android:id="@+id/checkboxUseDefaultText"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="4dp"
|
android:layout_marginBottom="4dp"
|
||||||
|
@ -186,14 +178,14 @@
|
||||||
app:layout_constraintStart_toStartOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/edittext_default_text"
|
android:id="@+id/editTextDefaultText"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:hint="@string/hint_default_text"
|
android:hint="@string/hint_default_text"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/checkbox_use_default_text"
|
app:layout_constraintStart_toEndOf="@id/checkboxUseDefaultText"
|
||||||
tools:ignore="Autofill" />
|
tools:ignore="Autofill" />
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
@ -214,7 +206,7 @@
|
||||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/action_photo_take"
|
android:id="@+id/actionPhotoTake"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:drawablePadding="8dp"
|
android:drawablePadding="8dp"
|
||||||
|
@ -223,7 +215,7 @@
|
||||||
android:textSize="?attr/status_text_medium" />
|
android:textSize="?attr/status_text_medium" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/action_photo_pick"
|
android:id="@+id/actionPhotoPick"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:drawablePadding="8dp"
|
android:drawablePadding="8dp"
|
||||||
|
@ -232,7 +224,7 @@
|
||||||
android:textSize="?attr/status_text_medium" />
|
android:textSize="?attr/status_text_medium" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/action_add_poll"
|
android:id="@+id/addPollTextActionTextView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:drawablePadding="8dp"
|
android:drawablePadding="8dp"
|
||||||
|
@ -257,7 +249,7 @@
|
||||||
app:behavior_peekHeight="0dp"
|
app:behavior_peekHeight="0dp"
|
||||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
|
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
|
||||||
|
|
||||||
<com.keylesspalace.tusky.view.ComposeOptionsView
|
<com.keylesspalace.tusky.components.compose.view.ComposeOptionsView
|
||||||
android:id="@+id/composeOptionsBottomSheet"
|
android:id="@+id/composeOptionsBottomSheet"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -271,7 +263,7 @@
|
||||||
app:behavior_peekHeight="0dp"
|
app:behavior_peekHeight="0dp"
|
||||||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
|
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" />
|
||||||
|
|
||||||
<com.keylesspalace.tusky.view.ComposeScheduleView
|
<com.keylesspalace.tusky.components.compose.view.ComposeScheduleView
|
||||||
android:id="@+id/composeScheduleView"
|
android:id="@+id/composeScheduleView"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
@ -340,7 +332,7 @@
|
||||||
android:contentDescription="@string/action_content_warning"
|
android:contentDescription="@string/action_content_warning"
|
||||||
android:padding="4dp"
|
android:padding="4dp"
|
||||||
android:tooltipText="@string/action_content_warning"
|
android:tooltipText="@string/action_content_warning"
|
||||||
app:srcCompat="@drawable/ic_cw_24dp"/>
|
app:srcCompat="@drawable/ic_cw_24dp" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/composeEmojiButton"
|
android:id="@+id/composeEmojiButton"
|
||||||
|
@ -377,7 +369,7 @@
|
||||||
android:textSize="?attr/status_text_medium"
|
android:textSize="?attr/status_text_medium"
|
||||||
tools:text="500" />
|
tools:text="500" />
|
||||||
|
|
||||||
<com.keylesspalace.tusky.view.TootButton
|
<com.keylesspalace.tusky.components.compose.view.TootButton
|
||||||
android:id="@+id/composeTootButton"
|
android:id="@+id/composeTootButton"
|
||||||
style="@style/TuskyButton"
|
style="@style/TuskyButton"
|
||||||
android:layout_width="@dimen/toot_button_width"
|
android:layout_width="@dimen/toot_button_width"
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
android:background="?android:colorBackground"
|
android:background="?android:colorBackground"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingStart="16dp"
|
android:paddingStart="16dp"
|
||||||
android:paddingTop="16dp"
|
android:paddingTop="8dp"
|
||||||
android:paddingEnd="16dp">
|
android:paddingEnd="16dp">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
|
@ -15,6 +15,8 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="end"
|
android:layout_gravity="end"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
android:src="@drawable/ic_drag_indicator_24dp"
|
android:src="@drawable/ic_drag_indicator_24dp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
@ -24,6 +26,8 @@
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="8dp"
|
android:layout_marginStart="8dp"
|
||||||
|
android:paddingTop="8dp"
|
||||||
|
android:paddingBottom="8dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:drawablePadding="12dp"
|
android:drawablePadding="12dp"
|
||||||
android:textColor="?android:attr/textColorSecondary"
|
android:textColor="?android:attr/textColorSecondary"
|
||||||
|
@ -32,10 +36,23 @@
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintStart_toEndOf="@id/imageView"
|
app:layout_constraintStart_toEndOf="@id/imageView"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:layout_goneMarginBottom="16dp"
|
app:layout_goneMarginBottom="8dp"
|
||||||
tools:drawableStart="@drawable/ic_home_24dp"
|
tools:drawableStart="@drawable/ic_home_24dp"
|
||||||
tools:text="Home" />
|
tools:text="Home" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/removeButton"
|
||||||
|
style="?attr/image_button_style"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:background="?attr/selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/action_delete"
|
||||||
|
android:src="@drawable/ic_clear_24dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<com.google.android.material.chip.ChipGroup
|
<com.google.android.material.chip.ChipGroup
|
||||||
android:id="@+id/chipGroup"
|
android:id="@+id/chipGroup"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -56,4 +73,3 @@
|
||||||
</com.google.android.material.chip.ChipGroup>
|
</com.google.android.material.chip.ChipGroup>
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
|
@ -5,27 +5,28 @@
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/resetScheduleButton"
|
android:id="@+id/resetScheduleButton"
|
||||||
|
style="@style/TuskyButton.Outlined"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingStart="16dp"
|
android:layout_marginEnd="16dp"
|
||||||
android:paddingEnd="16dp"
|
|
||||||
android:text="@string/action_reset_schedule"
|
android:text="@string/action_reset_schedule"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintBottom_toBottomOf="parent" />
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/scheduledDateTime"
|
android:id="@+id/scheduledDateTime"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:paddingBottom="16dp"
|
android:drawablePadding="4dp"
|
||||||
android:paddingEnd="16dp"
|
|
||||||
android:paddingStart="4dp"
|
android:paddingStart="4dp"
|
||||||
android:paddingTop="4dp"
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
android:textColor="?android:textColorTertiary"
|
android:textColor="?android:textColorTertiary"
|
||||||
android:textSize="?attr/status_text_medium"
|
android:textSize="?attr/status_text_medium"
|
||||||
android:drawablePadding="4dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="1"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/resetScheduleButton"
|
||||||
tools:text="2020/01/01 00:00:00" />
|
tools:text="2020/01/01 00:00:00" />
|
||||||
|
|
||||||
</merge>
|
</merge>
|
|
@ -1,9 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
<background android:drawable="@color/ic_launcher_background"/>
|
|
||||||
<foreground>
|
|
||||||
<inset
|
|
||||||
android:inset="28%"
|
|
||||||
android:drawable="@drawable/ic_create_24dp" />
|
|
||||||
</foreground>
|
|
||||||
</adaptive-icon>
|
|
|
@ -487,7 +487,6 @@
|
||||||
<string name="action_access_scheduled_toot">التبويقات المبَرمَجة</string>
|
<string name="action_access_scheduled_toot">التبويقات المبَرمَجة</string>
|
||||||
<string name="action_schedule_toot">برمجة تبويق</string>
|
<string name="action_schedule_toot">برمجة تبويق</string>
|
||||||
<string name="action_reset_schedule">صفّر</string>
|
<string name="action_reset_schedule">صفّر</string>
|
||||||
<string name="hint_configure_scheduled_toot">اضغط هنا لضبط برمجة التبويق.</string>
|
|
||||||
<string name="post_lookup_error_format">خطأ أثناء البحث عن منشور %s</string>
|
<string name="post_lookup_error_format">خطأ أثناء البحث عن منشور %s</string>
|
||||||
|
|
||||||
<string name="title_bookmarks">الفواصل المرجعية</string>
|
<string name="title_bookmarks">الفواصل المرجعية</string>
|
||||||
|
@ -495,4 +494,6 @@
|
||||||
<string name="action_view_bookmarks">الفواصل المرجعية</string>
|
<string name="action_view_bookmarks">الفواصل المرجعية</string>
|
||||||
<string name="about_powered_by_tusky">مدعوم بِـ Tusky</string>
|
<string name="about_powered_by_tusky">مدعوم بِـ Tusky</string>
|
||||||
<string name="description_status_bookmarked">أضيف إلى الفواصل المرجعية</string>
|
<string name="description_status_bookmarked">أضيف إلى الفواصل المرجعية</string>
|
||||||
|
<string name="select_list_title">اختر قائمة</string>
|
||||||
|
<string name="list">القائمة</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -508,7 +508,6 @@
|
||||||
<string name="action_access_scheduled_toot">নির্ধারিত টুটগুলি</string>
|
<string name="action_access_scheduled_toot">নির্ধারিত টুটগুলি</string>
|
||||||
<string name="action_schedule_toot">নির্ধারিত টুট</string>
|
<string name="action_schedule_toot">নির্ধারিত টুট</string>
|
||||||
<string name="action_reset_schedule">রিসেট</string>
|
<string name="action_reset_schedule">রিসেট</string>
|
||||||
<string name="hint_configure_scheduled_toot">নির্ধারিত টুট কনফিগার করতে এখানে আলতো চাপুন।</string>
|
|
||||||
<string name="about_powered_by_tusky">টাস্কি দ্বারা চালিত</string>
|
<string name="about_powered_by_tusky">টাস্কি দ্বারা চালিত</string>
|
||||||
<string name="post_lookup_error_format">%s পোস্ট অনুসন্ধানে ত্রুটি</string>
|
<string name="post_lookup_error_format">%s পোস্ট অনুসন্ধানে ত্রুটি</string>
|
||||||
|
|
||||||
|
|
|
@ -518,4 +518,18 @@
|
||||||
<string name="add_poll_choice">Afegeix una tria</string>
|
<string name="add_poll_choice">Afegeix una tria</string>
|
||||||
<string name="poll_allow_multiple_choices">Múltiples tries</string>
|
<string name="poll_allow_multiple_choices">Múltiples tries</string>
|
||||||
<string name="poll_new_choice_hint">Tria %d</string>
|
<string name="poll_new_choice_hint">Tria %d</string>
|
||||||
</resources>
|
<string name="title_bookmarks">Preferits</string>
|
||||||
|
<string name="title_scheduled_toot">Toots programats</string>
|
||||||
|
<string name="action_bookmark">Preferit</string>
|
||||||
|
<string name="action_edit">Editar</string>
|
||||||
|
<string name="action_view_bookmarks">Preferits</string>
|
||||||
|
<string name="action_access_scheduled_toot">Toots programats</string>
|
||||||
|
<string name="action_schedule_toot">Programar el toot</string>
|
||||||
|
<string name="action_reset_schedule">Reiniciar</string>
|
||||||
|
<string name="about_powered_by_tusky">Desenvolupat per Tusky</string>
|
||||||
|
<string name="description_status_bookmarked">Afegit a les adreces d\'interès</string>
|
||||||
|
<string name="select_list_title">Seleccionar la llista</string>
|
||||||
|
<string name="list">Llista</string>
|
||||||
|
<string name="post_lookup_error_format">S\'ha produït un error en cercar la publicació %s</string>
|
||||||
|
|
||||||
|
</resources>
|
||||||
|
|
|
@ -470,7 +470,6 @@
|
||||||
<string name="action_access_scheduled_toot">Plánované tooty</string>
|
<string name="action_access_scheduled_toot">Plánované tooty</string>
|
||||||
<string name="action_schedule_toot">Naplánovat toot</string>
|
<string name="action_schedule_toot">Naplánovat toot</string>
|
||||||
<string name="action_reset_schedule">Obnovit</string>
|
<string name="action_reset_schedule">Obnovit</string>
|
||||||
<string name="hint_configure_scheduled_toot">Klepnutím sem nastavíte plánovaný toot.</string>
|
|
||||||
<string name="pref_title_alway_open_spoiler">Vždy rozbalovat tooty označené varováními o obsahu</string>
|
<string name="pref_title_alway_open_spoiler">Vždy rozbalovat tooty označené varováními o obsahu</string>
|
||||||
<string name="filter_dialog_whole_word">Celé slovo</string>
|
<string name="filter_dialog_whole_word">Celé slovo</string>
|
||||||
<string name="filter_dialog_whole_word_description">Je-li klíčové slovo nebo fráze pouze alfanumerická, bude použita pouze, pokud odpovídá celému slovu</string>
|
<string name="filter_dialog_whole_word_description">Je-li klíčové slovo nebo fráze pouze alfanumerická, bude použita pouze, pokud odpovídá celému slovu</string>
|
||||||
|
|
|
@ -451,6 +451,5 @@
|
||||||
<string name="action_access_scheduled_toot">Geplante Beiträge</string>
|
<string name="action_access_scheduled_toot">Geplante Beiträge</string>
|
||||||
<string name="action_schedule_toot">Plane Beitrag</string>
|
<string name="action_schedule_toot">Plane Beitrag</string>
|
||||||
<string name="action_reset_schedule">Zurücksetzen</string>
|
<string name="action_reset_schedule">Zurücksetzen</string>
|
||||||
<string name="hint_configure_scheduled_toot">Drücke hier, um den geplanten Beitrag zu konfigurieren.</string>
|
|
||||||
<string name="abbreviated_in_years">Dies sind Zeitstempel für Status. Beispiele: \"16s\" oder \"2t\".</string>
|
<string name="abbreviated_in_years">Dies sind Zeitstempel für Status. Beispiele: \"16s\" oder \"2t\".</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -456,4 +456,18 @@
|
||||||
<string name="poll_new_choice_hint">Elekton %d</string>
|
<string name="poll_new_choice_hint">Elekton %d</string>
|
||||||
<string name="edit_poll">Redaktigi</string>
|
<string name="edit_poll">Redaktigi</string>
|
||||||
|
|
||||||
|
<string name="title_bookmarks">Legosignoj</string>
|
||||||
|
<string name="title_scheduled_toot">Planitaj mesaĝoj</string>
|
||||||
|
<string name="action_bookmark">Aldoni al legosignoj</string>
|
||||||
|
<string name="action_edit">Redakti</string>
|
||||||
|
<string name="action_view_bookmarks">Legosignoj</string>
|
||||||
|
<string name="action_access_scheduled_toot">Planitaj mesaĝoj</string>
|
||||||
|
<string name="action_schedule_toot">Plani mesaĝon</string>
|
||||||
|
<string name="action_reset_schedule">Restarigi</string>
|
||||||
|
<string name="about_powered_by_tusky">Funkciigita de Tusky</string>
|
||||||
|
<string name="description_status_bookmarked">Aldonita al la legosignoj</string>
|
||||||
|
<string name="select_list_title">Elekti la liston</string>
|
||||||
|
<string name="list">Listo</string>
|
||||||
|
<string name="post_lookup_error_format">Eraro dum elserĉo de la mesaĝo %s</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -465,7 +465,6 @@
|
||||||
<string name="action_access_scheduled_toot">Estados programados</string>
|
<string name="action_access_scheduled_toot">Estados programados</string>
|
||||||
<string name="action_schedule_toot">Programar estado</string>
|
<string name="action_schedule_toot">Programar estado</string>
|
||||||
<string name="action_reset_schedule">Reiniciar</string>
|
<string name="action_reset_schedule">Reiniciar</string>
|
||||||
<string name="hint_configure_scheduled_toot">Pulsa aquí para configurar un estado programado.</string>
|
|
||||||
<string name="post_lookup_error_format">Error al buscar el post %s</string>
|
<string name="post_lookup_error_format">Error al buscar el post %s</string>
|
||||||
|
|
||||||
<string name="about_powered_by_tusky">Potenciado por Tusky</string>
|
<string name="about_powered_by_tusky">Potenciado por Tusky</string>
|
||||||
|
@ -473,4 +472,6 @@
|
||||||
<string name="action_bookmark">Favorito</string>
|
<string name="action_bookmark">Favorito</string>
|
||||||
<string name="action_view_bookmarks">Favoritos</string>
|
<string name="action_view_bookmarks">Favoritos</string>
|
||||||
<string name="description_status_bookmarked">Marcado como favorito</string>
|
<string name="description_status_bookmarked">Marcado como favorito</string>
|
||||||
|
<string name="select_list_title">Seleccionar lista</string>
|
||||||
|
<string name="list">Lista</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -311,7 +311,6 @@
|
||||||
|
|
||||||
<string name="confirmation_domain_unmuted">%s ez dago ezkutatua</string>
|
<string name="confirmation_domain_unmuted">%s ez dago ezkutatua</string>
|
||||||
|
|
||||||
<string name="hint_configure_scheduled_toot">Sakatu hemen programatutako tuta konfiguratzeko.</string>
|
|
||||||
<string name="dialog_redraft_toot_warning">Tut hau ezabatu eta zirriborro berria egin\?</string>
|
<string name="dialog_redraft_toot_warning">Tut hau ezabatu eta zirriborro berria egin\?</string>
|
||||||
<string name="mute_domain_warning">Ziur al zaude %s ezabatu nahi duzula\? Domeinu horretatik datorren edukia ez duzu denbora-lerro publikoetan edo jakinarazpenentan ikusiko. Domeinu horretan dituzun jarraitzaileak ezabatuko dira.</string>
|
<string name="mute_domain_warning">Ziur al zaude %s ezabatu nahi duzula\? Domeinu horretatik datorren edukia ez duzu denbora-lerro publikoetan edo jakinarazpenentan ikusiko. Domeinu horretan dituzun jarraitzaileak ezabatuko dira.</string>
|
||||||
<string name="mute_domain_warning_dialog_ok">Domeinu osoa ezkutatu</string>
|
<string name="mute_domain_warning_dialog_ok">Domeinu osoa ezkutatu</string>
|
||||||
|
|
|
@ -460,7 +460,6 @@
|
||||||
<string name="action_access_scheduled_toot">بوقهای زمانبندیشده</string>
|
<string name="action_access_scheduled_toot">بوقهای زمانبندیشده</string>
|
||||||
<string name="action_schedule_toot">زمانبندی بوق</string>
|
<string name="action_schedule_toot">زمانبندی بوق</string>
|
||||||
<string name="action_reset_schedule">بازنشانی</string>
|
<string name="action_reset_schedule">بازنشانی</string>
|
||||||
<string name="hint_configure_scheduled_toot">برای پیکربندی بوق زمانبندیشده، اینجا را بزنید.</string>
|
|
||||||
<string name="mute_domain_warning">مطمئنید میخواهید تمام %s را مسدود کنید؟ محتوای آن دامنه را در هیچیک از خط زمانیها یا در آگاهیهایتان نخواهید دید. پیروانتان از آن دامنه، برداشته خواهند شد.</string>
|
<string name="mute_domain_warning">مطمئنید میخواهید تمام %s را مسدود کنید؟ محتوای آن دامنه را در هیچیک از خط زمانیها یا در آگاهیهایتان نخواهید دید. پیروانتان از آن دامنه، برداشته خواهند شد.</string>
|
||||||
<string name="filter_dialog_whole_word_description">هنگامی که کلیدواژه یا عبارت، فقط حروفعددی باشد، فقط اگر با تمام واژه مطابق باشد، اعمال خواهد شد</string>
|
<string name="filter_dialog_whole_word_description">هنگامی که کلیدواژه یا عبارت، فقط حروفعددی باشد، فقط اگر با تمام واژه مطابق باشد، اعمال خواهد شد</string>
|
||||||
<string name="filter_add_description">عبارت پالایش</string>
|
<string name="filter_add_description">عبارت پالایش</string>
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<string name="title_direct_messages">Messages directs</string>
|
<string name="title_direct_messages">Messages directs</string>
|
||||||
<string name="title_tab_preferences">Onglets</string>
|
<string name="title_tab_preferences">Onglets</string>
|
||||||
<string name="title_view_thread">Pouet</string>
|
<string name="title_view_thread">Pouet</string>
|
||||||
<string name="title_statuses">Pouets</string>
|
<string name="title_statuses">Messages</string>
|
||||||
<string name="title_statuses_with_replies">Pouets & réponses</string>
|
<string name="title_statuses_with_replies">Pouets & réponses</string>
|
||||||
<string name="title_statuses_pinned">Épinglés</string>
|
<string name="title_statuses_pinned">Épinglés</string>
|
||||||
<string name="title_follows">Abonnements</string>
|
<string name="title_follows">Abonnements</string>
|
||||||
|
@ -170,8 +170,8 @@
|
||||||
<string name="pref_title_notification_filters">Me notifier lorsque</string>
|
<string name="pref_title_notification_filters">Me notifier lorsque</string>
|
||||||
<string name="pref_title_notification_filter_mentions">on me mentionne</string>
|
<string name="pref_title_notification_filter_mentions">on me mentionne</string>
|
||||||
<string name="pref_title_notification_filter_follows">on me suit</string>
|
<string name="pref_title_notification_filter_follows">on me suit</string>
|
||||||
<string name="pref_title_notification_filter_reblogs">mes pouets sont boostés</string>
|
<string name="pref_title_notification_filter_reblogs">mes messages sont boostés</string>
|
||||||
<string name="pref_title_notification_filter_favourites">mes pouets sont mis en favoris</string>
|
<string name="pref_title_notification_filter_favourites">mes messages sont mis en favoris</string>
|
||||||
<string name="pref_title_appearance_settings">Apparence</string>
|
<string name="pref_title_appearance_settings">Apparence</string>
|
||||||
<string name="pref_title_app_theme">Thème de l’application</string>
|
<string name="pref_title_app_theme">Thème de l’application</string>
|
||||||
<string name="pref_title_timelines">Fils chronologiques</string>
|
<string name="pref_title_timelines">Fils chronologiques</string>
|
||||||
|
@ -472,12 +472,13 @@
|
||||||
<string name="action_access_scheduled_toot">Pouets planifiés</string>
|
<string name="action_access_scheduled_toot">Pouets planifiés</string>
|
||||||
<string name="action_schedule_toot">Planifier le pouet</string>
|
<string name="action_schedule_toot">Planifier le pouet</string>
|
||||||
<string name="action_reset_schedule">Réinitialiser</string>
|
<string name="action_reset_schedule">Réinitialiser</string>
|
||||||
<string name="hint_configure_scheduled_toot">Appuyez ici pour configurer le pouet planifié.</string>
|
<string name="post_lookup_error_format">Erreur lors de la récupération du message %s</string>
|
||||||
<string name="post_lookup_error_format">Erreur lors de la recherche du post %s</string>
|
|
||||||
|
|
||||||
<string name="about_powered_by_tusky">Propulsé par Tusky</string>
|
<string name="about_powered_by_tusky">Propulsé par Tusky</string>
|
||||||
<string name="title_bookmarks">Signets</string>
|
<string name="title_bookmarks">Marque-pages</string>
|
||||||
<string name="action_bookmark">Marquer comme signet</string>
|
<string name="action_bookmark">Ajouter aux marque-pages</string>
|
||||||
<string name="action_view_bookmarks">Signets</string>
|
<string name="action_view_bookmarks">Marque-pages</string>
|
||||||
<string name="description_status_bookmarked">Marqué comme un signet</string>
|
<string name="description_status_bookmarked">Ajouté aux marque-pages</string>
|
||||||
|
<string name="select_list_title">Sélectionner la liste</string>
|
||||||
|
<string name="list">Liste</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -470,7 +470,13 @@
|
||||||
<string name="action_access_scheduled_toot">Időzített tülkök</string>
|
<string name="action_access_scheduled_toot">Időzített tülkök</string>
|
||||||
<string name="action_schedule_toot">Tülk Időzítése</string>
|
<string name="action_schedule_toot">Tülk Időzítése</string>
|
||||||
<string name="action_reset_schedule">Visszaállítás</string>
|
<string name="action_reset_schedule">Visszaállítás</string>
|
||||||
<string name="hint_configure_scheduled_toot">Ide nyúlj az időzített tülkök beállításához.</string>
|
|
||||||
<string name="post_lookup_error_format">Nem találjuk ezt a posztot %s</string>
|
<string name="post_lookup_error_format">Nem találjuk ezt a posztot %s</string>
|
||||||
|
|
||||||
</resources>
|
<string name="title_bookmarks">Könyvjelzők</string>
|
||||||
|
<string name="action_bookmark">Könyvjelző</string>
|
||||||
|
<string name="action_view_bookmarks">Könyvjelzők</string>
|
||||||
|
<string name="about_powered_by_tusky">Tusky által hatjva</string>
|
||||||
|
<string name="description_status_bookmarked">Könyvjelzőzve</string>
|
||||||
|
<string name="select_list_title">Lista kiválasztása</string>
|
||||||
|
<string name="list">Lista</string>
|
||||||
|
</resources>
|
||||||
|
|
|
@ -325,9 +325,9 @@
|
||||||
<string name="unpin_action">Non fissare</string>
|
<string name="unpin_action">Non fissare</string>
|
||||||
<string name="pin_action">Fissa</string>
|
<string name="pin_action">Fissa</string>
|
||||||
<plurals name="favs">
|
<plurals name="favs">
|
||||||
<item quantity="one"><b>%1$s</b> Mi piace</item>
|
<item quantity="one"><b>%1$s</b> Mi piace</item>
|
||||||
<item quantity="other"><b>%1$s</b> Mi piace</item>
|
<item quantity="other"><b>%1$s</b> Mi piace</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="reblogs">
|
<plurals name="reblogs">
|
||||||
<item quantity="one"><b>%s</b> Boost</item>
|
<item quantity="one"><b>%s</b> Boost</item>
|
||||||
<item quantity="other"><b>%s</b> Boost</item>
|
<item quantity="other"><b>%s</b> Boost</item>
|
||||||
|
@ -392,9 +392,9 @@
|
||||||
<string name="title_domain_mutes">Domini nascosti</string>
|
<string name="title_domain_mutes">Domini nascosti</string>
|
||||||
<string name="action_view_domain_mutes">Domini nascosti</string>
|
<string name="action_view_domain_mutes">Domini nascosti</string>
|
||||||
<string name="action_mute_domain">Silenzia %s</string>
|
<string name="action_mute_domain">Silenzia %s</string>
|
||||||
<string name="confirmation_domain_unmuted">%s</string>
|
<string name="confirmation_domain_unmuted">%s mostrati</string>
|
||||||
|
|
||||||
<string name="mute_domain_warning">Sei sicuro di voler bloccare tutto %s\? Non vedrai nessun contenuto da quel dominio in nessuna timeline pubblica o nelle tue notifiche. I tuoi seguaci che stanno in quel dominio saranno rimossi</string>
|
<string name="mute_domain_warning">Sei sicuro di voler bloccare tutto %s\? Non vedrai nessun contenuto da quel dominio in nessuna timeline pubblica o nelle tue notifiche. I tuoi seguaci che stanno in quel dominio saranno rimossi.</string>
|
||||||
<string name="mute_domain_warning_dialog_ok">Nascondi l\'intero dominio</string>
|
<string name="mute_domain_warning_dialog_ok">Nascondi l\'intero dominio</string>
|
||||||
|
|
||||||
<string name="pref_title_notification_filter_poll">Le votazioni sono finite</string>
|
<string name="pref_title_notification_filter_poll">Le votazioni sono finite</string>
|
||||||
|
@ -405,7 +405,76 @@
|
||||||
|
|
||||||
|
|
||||||
<string name="filter_dialog_whole_word">Parola intera</string>
|
<string name="filter_dialog_whole_word">Parola intera</string>
|
||||||
<string name="filter_dialog_whole_word_description">Quando la parola chiave o la frase sono composte da caratteri alfanumerici, sara\' applicata solo se corrisponde alla parola completa</string>
|
<string name="filter_dialog_whole_word_description">Quando la parola chiave o la frase sono composte da soli caratteri alfanumerici, sarà applicata solo se corrisponde alla parola completa</string>
|
||||||
<string name="caption_notoemoji">Insieme di emoji di Google</string>
|
<string name="caption_notoemoji">Insieme di emoji di Google</string>
|
||||||
|
|
||||||
|
<string name="title_bookmarks">Segnalibri</string>
|
||||||
|
<string name="action_bookmark">Segnalibro</string>
|
||||||
|
<string name="action_edit">Modifica</string>
|
||||||
|
<string name="action_view_bookmarks">Segnalibri</string>
|
||||||
|
<string name="action_add_poll">Aggiungi sondaggio</string>
|
||||||
|
<string name="about_powered_by_tusky">Fatto con Tusky</string>
|
||||||
|
<string name="pref_title_alway_open_spoiler">Espandi sempre i toot segnalati come contenuto sensibile</string>
|
||||||
|
<string name="description_status_bookmarked">Messo nei segnalibri</string>
|
||||||
|
<string name="description_poll">Sondaggio con scelte: %1$s, %2$s, %3$s, %4$s; %5$s</string>
|
||||||
|
|
||||||
|
<string name="select_list_title">Scegli lista</string>
|
||||||
|
<string name="list">Lista</string>
|
||||||
|
<string name="compose_preview_image_description">Azioni per l\'immagine %s</string>
|
||||||
|
|
||||||
|
<string name="poll_ended_voted">Un sondaggio che hai votato è terminato</string>
|
||||||
|
<string name="poll_ended_created">Un sondaggio che hai creato è terminato</string>
|
||||||
|
|
||||||
|
<plurals name="poll_timespan_days">
|
||||||
|
<item quantity="one">%d giorno</item>
|
||||||
|
<item quantity="other">%d giorni</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="poll_timespan_hours">
|
||||||
|
<item quantity="one">%d ora</item>
|
||||||
|
<item quantity="other">%d ore</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="poll_timespan_minutes">
|
||||||
|
<item quantity="one">%d minuto</item>
|
||||||
|
<item quantity="other">%d minuti</item>
|
||||||
|
</plurals>
|
||||||
|
<plurals name="poll_timespan_seconds">
|
||||||
|
<item quantity="one">%d secondo</item>
|
||||||
|
<item quantity="other">%d secondi</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
|
<string name="button_continue">Continua</string>
|
||||||
|
<string name="button_back">Indietro</string>
|
||||||
|
<string name="button_done">Fatto</string>
|
||||||
|
<string name="report_sent_success">Inviato con successo @%s</string>
|
||||||
|
<string name="hint_additional_info">Altri commenti</string>
|
||||||
|
<string name="report_remote_instance">Inoltra a %s</string>
|
||||||
|
<string name="failed_report">Errore durante l\'invio</string>
|
||||||
|
<string name="failed_fetch_statuses">Errore durante lo scaricamento degli aggiornamenti</string>
|
||||||
|
<string name="report_description_1">La segnalazione sarà inviata al moderatore del tuo server. Puoi spiegare perchè vuoi segnalare questo utente qui sotto:</string>
|
||||||
|
<string name="report_description_remote_instance">L\'utente è su un altro server. Mandare una copia della segnalazione anche lì\?</string>
|
||||||
|
<string name="title_accounts">Utenti</string>
|
||||||
|
<string name="failed_search">Errore durante la ricerca</string>
|
||||||
|
|
||||||
|
<string name="pref_title_show_notifications_filter">Mostra il filtro delle notifiche</string>
|
||||||
|
|
||||||
|
|
||||||
|
<string name="create_poll_title">Sondaggio</string>
|
||||||
|
<string name="poll_duration_5_min">5 minuti</string>
|
||||||
|
<string name="poll_duration_30_min">30 minuti</string>
|
||||||
|
<string name="poll_duration_1_hour">1 ora</string>
|
||||||
|
<string name="poll_duration_6_hours">6 ore</string>
|
||||||
|
<string name="poll_duration_1_day">1 giorno</string>
|
||||||
|
<string name="poll_duration_3_days">3 giorni</string>
|
||||||
|
<string name="poll_duration_7_days">7 giorni</string>
|
||||||
|
<string name="add_poll_choice">Aggiungi scelta</string>
|
||||||
|
<string name="poll_allow_multiple_choices">Scelte multiple</string>
|
||||||
|
<string name="poll_new_choice_hint">Scelta %d</string>
|
||||||
|
<string name="edit_poll">Modifica</string>
|
||||||
|
<string name="post_lookup_error_format">Errore nella ricerca del post %s</string>
|
||||||
|
|
||||||
|
<string name="title_scheduled_toot">Toot programmati</string>
|
||||||
|
<string name="action_access_scheduled_toot">Toot programmati</string>
|
||||||
|
<string name="action_schedule_toot">Programma un toot</string>
|
||||||
|
<string name="action_reset_schedule">RIpristina</string>
|
||||||
|
<string name="poll_info_format"> <!-- 15 votes • 1 hour left --> %1$s • %2$s</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
<item name="account_toolbar_icon_tint_collapsed">@color/account_toolbar_icon_collapsed_dark</item>
|
<item name="account_toolbar_icon_tint_collapsed">@color/account_toolbar_icon_collapsed_dark</item>
|
||||||
|
|
||||||
<item name="compose_close_button_tint">@color/toolbar_icon_dark</item>
|
<item name="compose_close_button_tint">@color/toolbar_icon_dark</item>
|
||||||
<item name="compose_media_button_disabled_tint">@color/compose_media_button_disabled_dark</item>
|
<item name="image_button_disabled_tint">@color/image_button_disabled_dark</item>
|
||||||
<item name="compose_content_warning_bar_background">@drawable/border_background_dark</item>
|
<item name="compose_content_warning_bar_background">@drawable/border_background_dark</item>
|
||||||
<item name="compose_reply_content_background">@color/compose_reply_content_background_dark</item>
|
<item name="compose_reply_content_background">@color/compose_reply_content_background_dark</item>
|
||||||
|
|
||||||
|
|
|
@ -507,7 +507,6 @@
|
||||||
<string name="action_access_scheduled_toot">Planlagte toots</string>
|
<string name="action_access_scheduled_toot">Planlagte toots</string>
|
||||||
<string name="action_schedule_toot">Planlegg toot</string>
|
<string name="action_schedule_toot">Planlegg toot</string>
|
||||||
<string name="action_reset_schedule">Tilbakestill</string>
|
<string name="action_reset_schedule">Tilbakestill</string>
|
||||||
<string name="hint_configure_scheduled_toot">Klikk her for å konfigurere planlagt toot.</string>
|
|
||||||
<string name="post_lookup_error_format">Det oppsto en feil under henting av %s</string>
|
<string name="post_lookup_error_format">Det oppsto en feil under henting av %s</string>
|
||||||
|
|
||||||
<string name="about_powered_by_tusky">Drevet av Tusky</string>
|
<string name="about_powered_by_tusky">Drevet av Tusky</string>
|
||||||
|
@ -515,4 +514,6 @@
|
||||||
<string name="action_bookmark">Bokmerke</string>
|
<string name="action_bookmark">Bokmerke</string>
|
||||||
<string name="action_view_bookmarks">Bokmerker</string>
|
<string name="action_view_bookmarks">Bokmerker</string>
|
||||||
<string name="description_status_bookmarked">Bokmerke lagt til</string>
|
<string name="description_status_bookmarked">Bokmerke lagt til</string>
|
||||||
|
<string name="select_list_title">Velg liste</string>
|
||||||
|
<string name="list">Liste</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -477,7 +477,6 @@
|
||||||
<string name="action_access_scheduled_toot">Tuts planificats</string>
|
<string name="action_access_scheduled_toot">Tuts planificats</string>
|
||||||
<string name="action_schedule_toot">Planificar de tuts</string>
|
<string name="action_schedule_toot">Planificar de tuts</string>
|
||||||
<string name="action_reset_schedule">Escafar</string>
|
<string name="action_reset_schedule">Escafar</string>
|
||||||
<string name="hint_configure_scheduled_toot">Tocatz aquí per configurar los tuts planificats.</string>
|
|
||||||
<string name="post_lookup_error_format">Error en cercant la publicacion %s</string>
|
<string name="post_lookup_error_format">Error en cercant la publicacion %s</string>
|
||||||
|
|
||||||
<string name="about_powered_by_tusky">Propulsat per Tusky</string>
|
<string name="about_powered_by_tusky">Propulsat per Tusky</string>
|
||||||
|
@ -485,4 +484,6 @@
|
||||||
<string name="action_bookmark">Ajustar als marcapaginas</string>
|
<string name="action_bookmark">Ajustar als marcapaginas</string>
|
||||||
<string name="action_view_bookmarks">Marcapaginas</string>
|
<string name="action_view_bookmarks">Marcapaginas</string>
|
||||||
<string name="description_status_bookmarked">Ajustat als marcapaginas</string>
|
<string name="description_status_bookmarked">Ajustat als marcapaginas</string>
|
||||||
|
<string name="select_list_title">Seleccionar la list</string>
|
||||||
|
<string name="list">Lista</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -368,9 +368,9 @@
|
||||||
<string name="pin_action">Przypnij do profilu</string>
|
<string name="pin_action">Przypnij do profilu</string>
|
||||||
|
|
||||||
<plurals name="favs">
|
<plurals name="favs">
|
||||||
<item quantity="one"><b>%1$s</b> polubienie</item>
|
<item quantity="one"><b>%1$s</b> polubienie</item>
|
||||||
<item quantity="few"><b>%1$s</b> polubienia</item>
|
<item quantity="few"><b>%1$s</b> polubienia</item>
|
||||||
<item quantity="many"><b>%1$s</b> polubień</item>
|
<item quantity="many"><b>%1$s</b> polubień</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<plurals name="reblogs">
|
<plurals name="reblogs">
|
||||||
|
@ -484,7 +484,6 @@
|
||||||
<string name="action_access_scheduled_toot">Zaplanowane wpisy</string>
|
<string name="action_access_scheduled_toot">Zaplanowane wpisy</string>
|
||||||
<string name="action_schedule_toot">Zaplanuj wpis</string>
|
<string name="action_schedule_toot">Zaplanuj wpis</string>
|
||||||
<string name="action_reset_schedule">Resetuj</string>
|
<string name="action_reset_schedule">Resetuj</string>
|
||||||
<string name="hint_configure_scheduled_toot">Dotknij tutaj, żeby skonfigurować zaplanowany wpis.</string>
|
|
||||||
<string name="about_powered_by_tusky">Napędzane przez Tusky</string>
|
<string name="about_powered_by_tusky">Napędzane przez Tusky</string>
|
||||||
<string name="post_lookup_error_format">Błąd przy wyszukiwaniu wpisu %s</string>
|
<string name="post_lookup_error_format">Błąd przy wyszukiwaniu wpisu %s</string>
|
||||||
|
|
||||||
|
@ -492,4 +491,6 @@
|
||||||
<string name="action_bookmark">Zakładka</string>
|
<string name="action_bookmark">Zakładka</string>
|
||||||
<string name="action_view_bookmarks">Zakładki</string>
|
<string name="action_view_bookmarks">Zakładki</string>
|
||||||
<string name="description_status_bookmarked">Dodane do zakładek</string>
|
<string name="description_status_bookmarked">Dodane do zakładek</string>
|
||||||
|
<string name="select_list_title">Wybierz listę</string>
|
||||||
|
<string name="list">Lista</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -191,9 +191,9 @@
|
||||||
<string name="status_text_size_large">Grande</string>
|
<string name="status_text_size_large">Grande</string>
|
||||||
<string name="status_text_size_largest">Maior</string>
|
<string name="status_text_size_largest">Maior</string>
|
||||||
|
|
||||||
<string name="notification_mention_name">Novas Menções</string>
|
<string name="notification_mention_name">Menções</string>
|
||||||
<string name="notification_mention_descriptions">Notificar sobre novas menções</string>
|
<string name="notification_mention_descriptions">Notificar sobre novas menções</string>
|
||||||
<string name="notification_follow_name">Novos Seguidores</string>
|
<string name="notification_follow_name">Seguidores</string>
|
||||||
<string name="notification_follow_description">Notificar sobre novos seguidores</string>
|
<string name="notification_follow_description">Notificar sobre novos seguidores</string>
|
||||||
<string name="notification_boost_name">Boosts</string>
|
<string name="notification_boost_name">Boosts</string>
|
||||||
<string name="notification_boost_description">Notificar quando derem boost nos seus toots</string>
|
<string name="notification_boost_description">Notificar quando derem boost nos seus toots</string>
|
||||||
|
@ -342,7 +342,7 @@
|
||||||
<string name="action_remove_from_list">Remover conta da lista</string>
|
<string name="action_remove_from_list">Remover conta da lista</string>
|
||||||
|
|
||||||
<string name="hint_describe_for_visually_impaired">Descrever para deficientes visuais
|
<string name="hint_describe_for_visually_impaired">Descrever para deficientes visuais
|
||||||
\n(limite de %d caracteres)</string>
|
\n(até %d caracteres)</string>
|
||||||
<string name="license_cc_by_4">CC-BY 4.0</string>
|
<string name="license_cc_by_4">CC-BY 4.0</string>
|
||||||
<string name="license_cc_by_sa_4">CC-BY-SA 4.0</string>
|
<string name="license_cc_by_sa_4">CC-BY-SA 4.0</string>
|
||||||
|
|
||||||
|
@ -467,17 +467,18 @@
|
||||||
<string name="poll_new_choice_hint">Opção %d</string>
|
<string name="poll_new_choice_hint">Opção %d</string>
|
||||||
<string name="edit_poll">Editar</string>
|
<string name="edit_poll">Editar</string>
|
||||||
|
|
||||||
<string name="title_scheduled_toot">Toots agendados</string>
|
<string name="title_scheduled_toot">Agendados</string>
|
||||||
<string name="action_edit">Editar</string>
|
<string name="action_edit">Editar</string>
|
||||||
<string name="action_access_scheduled_toot">Toots agendados</string>
|
<string name="action_access_scheduled_toot">Agendados</string>
|
||||||
<string name="action_schedule_toot">Agendar toot</string>
|
<string name="action_schedule_toot">Agendar toot</string>
|
||||||
<string name="action_reset_schedule">Cancelar</string>
|
<string name="action_reset_schedule">Cancelar</string>
|
||||||
<string name="hint_configure_scheduled_toot">Toque aqui para agendar</string>
|
|
||||||
<string name="post_lookup_error_format">Erro ao pesquisar %s</string>
|
<string name="post_lookup_error_format">Erro ao pesquisar %s</string>
|
||||||
|
|
||||||
<string name="title_bookmarks">Salvos</string>
|
<string name="title_bookmarks">Salvos</string>
|
||||||
<string name="action_bookmark">Salvo</string>
|
<string name="action_bookmark">Salvar</string>
|
||||||
<string name="action_view_bookmarks">Salvos</string>
|
<string name="action_view_bookmarks">Salvos</string>
|
||||||
<string name="about_powered_by_tusky">Desenvolvido por Tusky</string>
|
<string name="about_powered_by_tusky">Desenvolvido por Tusky</string>
|
||||||
<string name="description_status_bookmarked">Salvo</string>
|
<string name="description_status_bookmarked">Salvo</string>
|
||||||
|
<string name="select_list_title">Selecionar lista</string>
|
||||||
|
<string name="list">Lista</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -540,7 +540,13 @@
|
||||||
<string name="action_access_scheduled_toot">Отложенные записи</string>
|
<string name="action_access_scheduled_toot">Отложенные записи</string>
|
||||||
<string name="action_schedule_toot">Отложить запись</string>
|
<string name="action_schedule_toot">Отложить запись</string>
|
||||||
<string name="action_reset_schedule">Сброс</string>
|
<string name="action_reset_schedule">Сброс</string>
|
||||||
<string name="hint_configure_scheduled_toot">Нажмите для выбора времени отправки.</string>
|
|
||||||
<string name="post_lookup_error_format">Ошибка при поиске сообщения / ний</string>
|
<string name="post_lookup_error_format">Ошибка при поиске сообщения / ний</string>
|
||||||
|
|
||||||
</resources>
|
<string name="title_bookmarks">Закладки</string>
|
||||||
|
<string name="action_bookmark">Добавить в закладки</string>
|
||||||
|
<string name="action_view_bookmarks">Закладки</string>
|
||||||
|
<string name="about_powered_by_tusky">Под управлением Tusky</string>
|
||||||
|
<string name="description_status_bookmarked">Добавлено в закладки</string>
|
||||||
|
<string name="select_list_title">Выбрать список</string>
|
||||||
|
<string name="list">Список</string>
|
||||||
|
</resources>
|
||||||
|
|
|
@ -521,7 +521,6 @@
|
||||||
<string name="action_access_scheduled_toot">Napovedani tuti</string>
|
<string name="action_access_scheduled_toot">Napovedani tuti</string>
|
||||||
<string name="action_reset_schedule">Ponastavi</string>
|
<string name="action_reset_schedule">Ponastavi</string>
|
||||||
<string name="action_schedule_toot">Napovej tut</string>
|
<string name="action_schedule_toot">Napovej tut</string>
|
||||||
<string name="hint_configure_scheduled_toot">Dotaknite se tukaj, da nastavite napovedan tut.</string>
|
|
||||||
<string name="post_lookup_error_format">Napaka pri iskanju objave %s</string>
|
<string name="post_lookup_error_format">Napaka pri iskanju objave %s</string>
|
||||||
|
|
||||||
<string name="about_powered_by_tusky">Poganja ga Tusky</string>
|
<string name="about_powered_by_tusky">Poganja ga Tusky</string>
|
||||||
|
|
|
@ -468,7 +468,6 @@
|
||||||
<string name="action_access_scheduled_toot">Schemalagda toots</string>
|
<string name="action_access_scheduled_toot">Schemalagda toots</string>
|
||||||
<string name="action_schedule_toot">Schemalägg toot</string>
|
<string name="action_schedule_toot">Schemalägg toot</string>
|
||||||
<string name="action_reset_schedule">Återställ</string>
|
<string name="action_reset_schedule">Återställ</string>
|
||||||
<string name="hint_configure_scheduled_toot">Knacka här för att konfigurera schemalagd toot.</string>
|
|
||||||
<string name="post_lookup_error_format">Fel vid uppslagning av status %s</string>
|
<string name="post_lookup_error_format">Fel vid uppslagning av status %s</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
<string name="message_empty">Burada hiçbir şey yok.</string>
|
<string name="message_empty">Burada hiçbir şey yok.</string>
|
||||||
<string name="footer_empty">Burada henüz hiçbir şey yok. Yenilemek için aşağıya çekin!</string>
|
<string name="footer_empty">Burada henüz hiçbir şey yok. Yenilemek için aşağıya çekin!</string>
|
||||||
<string name="notification_reblog_format">%s iletini yineledi</string>
|
<string name="notification_reblog_format">%s iletini yineledi</string>
|
||||||
<string name="notification_favourite_format">%s iletini favorilerine ekledi</string>
|
<string name="notification_favourite_format">%s ileti favorilerine ekledi</string>
|
||||||
<string name="notification_follow_format">%s seni takip etti</string>
|
<string name="notification_follow_format">%s seni takip etti</string>
|
||||||
<string name="report_username_format">\@%s bildir</string>
|
<string name="report_username_format">\@%s bildir</string>
|
||||||
<string name="report_comment_hint">Daha fazla yorum?</string>
|
<string name="report_comment_hint">Daha fazla yorum?</string>
|
||||||
|
@ -294,8 +294,8 @@
|
||||||
<item quantity="other"><b>%1$s</b> Favoriler</item>
|
<item quantity="other"><b>%1$s</b> Favoriler</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="reblogs">
|
<plurals name="reblogs">
|
||||||
<item quantity="one"><b>%s</b> Yinelenen</item>
|
<item quantity="one"><b>%s</b> Yinelenen</item>
|
||||||
<item quantity="other"><b>%s</b> Yinelenenler</item>
|
<item quantity="other"><b>%s</b> Yinelenenler</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="title_reblogged_by">tarafından yinelendi</string>
|
<string name="title_reblogged_by">tarafından yinelendi</string>
|
||||||
<string name="title_favourited_by">Tarafından favorilendi</string>
|
<string name="title_favourited_by">Tarafından favorilendi</string>
|
||||||
|
@ -425,4 +425,36 @@
|
||||||
<string name="title_mentions_dialog">Bahsedenler</string>
|
<string name="title_mentions_dialog">Bahsedenler</string>
|
||||||
<string name="action_open_media_n">#%d medyayı aç</string>
|
<string name="action_open_media_n">#%d medyayı aç</string>
|
||||||
|
|
||||||
|
<string name="title_bookmarks">Yer imleri</string>
|
||||||
|
<string name="title_scheduled_toot">Zamanlanmış iletiler</string>
|
||||||
|
<string name="action_bookmark">Yerimi</string>
|
||||||
|
<string name="action_edit">Düzenle</string>
|
||||||
|
<string name="action_delete_and_redraft">Sil ve düzenle</string>
|
||||||
|
<string name="action_view_bookmarks">Yer imleri</string>
|
||||||
|
<string name="action_add_poll">Anket ekle</string>
|
||||||
|
<string name="action_access_scheduled_toot">Zamanlanmış iletiler</string>
|
||||||
|
<string name="action_schedule_toot">İleti zamanla</string>
|
||||||
|
<string name="action_reset_schedule">Sıfırla</string>
|
||||||
|
<string name="dialog_redraft_toot_warning">Bu iletiyi silip yeniden düzenlemek istiyor musun\?</string>
|
||||||
|
<string name="pref_title_bot_overlay">Botlar için gösterge göster</string>
|
||||||
|
<string name="about_powered_by_tusky">Tusky tarafından desteklenmektedir</string>
|
||||||
|
<string name="description_status_bookmarked">Yerimine eklendi</string>
|
||||||
|
<string name="select_list_title">Liste seç</string>
|
||||||
|
<string name="list">Liste</string>
|
||||||
|
<string name="title_accounts">Hesaplar</string>
|
||||||
|
<string name="failed_search">Arama başarısız</string>
|
||||||
|
|
||||||
|
<string name="create_poll_title">Anket</string>
|
||||||
|
<string name="poll_duration_5_min">5 dakika</string>
|
||||||
|
<string name="poll_duration_30_min">30 dakika</string>
|
||||||
|
<string name="poll_duration_1_hour">1 saat</string>
|
||||||
|
<string name="poll_duration_6_hours">6 saat</string>
|
||||||
|
<string name="poll_duration_1_day">1 gün</string>
|
||||||
|
<string name="poll_duration_3_days">3 gün</string>
|
||||||
|
<string name="poll_duration_7_days">7 gün</string>
|
||||||
|
<string name="add_poll_choice">Seçenek ekle</string>
|
||||||
|
<string name="poll_allow_multiple_choices">Çoklu seçim</string>
|
||||||
|
<string name="edit_poll">Düzenle</string>
|
||||||
|
<string name="replying_to">\@%s olarak yanıtla</string>
|
||||||
|
<string name="profile_badge_bot_text">Bot</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -33,7 +33,7 @@
|
||||||
<attr name="account_toolbar_icon_tint_uncollapsed" format="reference|color" />
|
<attr name="account_toolbar_icon_tint_uncollapsed" format="reference|color" />
|
||||||
<attr name="account_toolbar_icon_tint_collapsed" format="reference|color" />
|
<attr name="account_toolbar_icon_tint_collapsed" format="reference|color" />
|
||||||
<attr name="compose_close_button_tint" format="reference|color" />
|
<attr name="compose_close_button_tint" format="reference|color" />
|
||||||
<attr name="compose_media_button_disabled_tint" format="reference|color" />
|
<attr name="image_button_disabled_tint" format="reference|color" />
|
||||||
<attr name="compose_content_warning_bar_background" format="reference" />
|
<attr name="compose_content_warning_bar_background" format="reference" />
|
||||||
<attr name="compose_reply_content_background" format="reference|color" />
|
<attr name="compose_reply_content_background" format="reference|color" />
|
||||||
<attr name="report_status_background_color" format="reference|color" />
|
<attr name="report_status_background_color" format="reference|color" />
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
<color name="status_divider_dark">#2f3441</color>
|
<color name="status_divider_dark">#2f3441</color>
|
||||||
<color name="tab_page_margin_dark">#1a1c23</color>
|
<color name="tab_page_margin_dark">#1a1c23</color>
|
||||||
<color name="account_toolbar_icon_collapsed_dark">#ffffff</color>
|
<color name="account_toolbar_icon_collapsed_dark">#ffffff</color>
|
||||||
<color name="compose_media_button_disabled_dark">#586173</color>
|
<color name="image_button_disabled_dark">#586173</color>
|
||||||
<color name="custom_tab_toolbar_dark">#313543</color>
|
<color name="custom_tab_toolbar_dark">#313543</color>
|
||||||
<color name="compose_reply_content_background_dark">#373c4b</color>
|
<color name="compose_reply_content_background_dark">#373c4b</color>
|
||||||
<color name="autocomplete_divider_dark">#424a5b</color>
|
<color name="autocomplete_divider_dark">#424a5b</color>
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
<color name="status_divider_light">#cfcfcf</color>
|
<color name="status_divider_light">#cfcfcf</color>
|
||||||
<color name="tab_page_margin_light">#cfcfcf</color>
|
<color name="tab_page_margin_light">#cfcfcf</color>
|
||||||
<color name="account_toolbar_icon_collapsed_light">#DE000000</color>
|
<color name="account_toolbar_icon_collapsed_light">#DE000000</color>
|
||||||
<color name="compose_media_button_disabled_light">#a3a5ab</color>
|
<color name="image_button_disabled_light">#a3a5ab</color>
|
||||||
<color name="report_status_background_light">#EFEFEF</color>
|
<color name="report_status_background_light">#EFEFEF</color>
|
||||||
<color name="custom_tab_toolbar_light">#ffffff</color>
|
<color name="custom_tab_toolbar_light">#ffffff</color>
|
||||||
<color name="compose_reply_content_background_light">#e0e1e6</color>
|
<color name="compose_reply_content_background_light">#e0e1e6</color>
|
||||||
|
|
|
@ -168,7 +168,6 @@
|
||||||
|
|
||||||
<string name="hint_domain">Which instance?</string>
|
<string name="hint_domain">Which instance?</string>
|
||||||
<string name="hint_compose">What\'s happening?</string>
|
<string name="hint_compose">What\'s happening?</string>
|
||||||
<string name="hint_configure_scheduled_toot">Tap here to configure scheduled toot.</string>
|
|
||||||
<string name="hint_content_warning">Content warning</string>
|
<string name="hint_content_warning">Content warning</string>
|
||||||
<string name="hint_display_name">Display name</string>
|
<string name="hint_display_name">Display name</string>
|
||||||
<string name="hint_note">Bio</string>
|
<string name="hint_note">Bio</string>
|
||||||
|
|
|
@ -108,7 +108,7 @@
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
<item name="compose_close_button_tint">@color/toolbar_icon_light</item>
|
<item name="compose_close_button_tint">@color/toolbar_icon_light</item>
|
||||||
<item name="compose_media_button_disabled_tint">@color/compose_media_button_disabled_light
|
<item name="image_button_disabled_tint">@color/image_button_disabled_light
|
||||||
</item>
|
</item>
|
||||||
<item name="compose_content_warning_bar_background">@drawable/border_background_light</item>
|
<item name="compose_content_warning_bar_background">@drawable/border_background_light</item>
|
||||||
<item name="compose_reply_content_background">
|
<item name="compose_reply_content_background">
|
||||||
|
|
|
@ -18,16 +18,21 @@ package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.text.SpannedString
|
import android.text.SpannedString
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||||
import com.keylesspalace.tusky.db.InstanceDao
|
import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT
|
||||||
|
import com.keylesspalace.tusky.components.compose.MediaUploader
|
||||||
|
import com.keylesspalace.tusky.db.*
|
||||||
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
|
||||||
import com.keylesspalace.tusky.entity.Instance
|
import com.keylesspalace.tusky.entity.Instance
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import okhttp3.Request
|
import com.keylesspalace.tusky.service.ServiceClient
|
||||||
import org.junit.Assert
|
import com.keylesspalace.tusky.util.SaveTootHelper
|
||||||
|
import com.nhaarman.mockitokotlin2.any
|
||||||
|
import io.reactivex.Single
|
||||||
|
import io.reactivex.SingleObserver
|
||||||
import org.junit.Assert.*
|
import org.junit.Assert.*
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
@ -35,15 +40,8 @@ import org.junit.runner.RunWith
|
||||||
import org.mockito.Mockito.`when`
|
import org.mockito.Mockito.`when`
|
||||||
import org.mockito.Mockito.mock
|
import org.mockito.Mockito.mock
|
||||||
import org.robolectric.Robolectric
|
import org.robolectric.Robolectric
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
import io.reactivex.Single
|
|
||||||
import io.reactivex.SingleObserver
|
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
import org.robolectric.fakes.RoboMenuItem
|
import org.robolectric.fakes.RoboMenuItem
|
||||||
import retrofit2.Call
|
|
||||||
import retrofit2.Callback
|
|
||||||
import retrofit2.Response
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by charlag on 3/7/18.
|
* Created by charlag on 3/7/18.
|
||||||
|
@ -52,14 +50,15 @@ import retrofit2.Response
|
||||||
@Config(application = FakeTuskyApplication::class, sdk = [28])
|
@Config(application = FakeTuskyApplication::class, sdk = [28])
|
||||||
@RunWith(AndroidJUnit4::class)
|
@RunWith(AndroidJUnit4::class)
|
||||||
class ComposeActivityTest {
|
class ComposeActivityTest {
|
||||||
|
|
||||||
private lateinit var activity: ComposeActivity
|
private lateinit var activity: ComposeActivity
|
||||||
private lateinit var accountManagerMock: AccountManager
|
private lateinit var accountManagerMock: AccountManager
|
||||||
private lateinit var apiMock: MastodonApi
|
private lateinit var apiMock: MastodonApi
|
||||||
|
|
||||||
|
private val instanceDomain = "example.domain"
|
||||||
|
|
||||||
private val account = AccountEntity(
|
private val account = AccountEntity(
|
||||||
id = 1,
|
id = 1,
|
||||||
domain = "example.token",
|
domain = instanceDomain,
|
||||||
accessToken = "token",
|
accessToken = "token",
|
||||||
isActive = true,
|
isActive = true,
|
||||||
accountId = "1",
|
accountId = "1",
|
||||||
|
@ -83,30 +82,10 @@ class ComposeActivityTest {
|
||||||
activity = controller.get()
|
activity = controller.get()
|
||||||
|
|
||||||
accountManagerMock = mock(AccountManager::class.java)
|
accountManagerMock = mock(AccountManager::class.java)
|
||||||
|
`when`(accountManagerMock.activeAccount).thenReturn(account)
|
||||||
|
|
||||||
apiMock = mock(MastodonApi::class.java)
|
apiMock = mock(MastodonApi::class.java)
|
||||||
`when`(apiMock.getCustomEmojis()).thenReturn(object: Call<List<Emoji>> {
|
`when`(apiMock.getCustomEmojis()).thenReturn(Single.just(emptyList()))
|
||||||
override fun isExecuted(): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
override fun clone(): Call<List<Emoji>> {
|
|
||||||
throw Error("not implemented")
|
|
||||||
}
|
|
||||||
override fun isCanceled(): Boolean {
|
|
||||||
throw Error("not implemented")
|
|
||||||
}
|
|
||||||
override fun cancel() {
|
|
||||||
throw Error("not implemented")
|
|
||||||
}
|
|
||||||
override fun execute(): Response<List<Emoji>> {
|
|
||||||
throw Error("not implemented")
|
|
||||||
}
|
|
||||||
override fun request(): Request {
|
|
||||||
throw Error("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun enqueue(callback: Callback<List<Emoji>>?) {}
|
|
||||||
})
|
|
||||||
`when`(apiMock.getInstance()).thenReturn(object: Single<Instance>() {
|
`when`(apiMock.getInstance()).thenReturn(object: Single<Instance>() {
|
||||||
override fun subscribeActual(observer: SingleObserver<in Instance>) {
|
override fun subscribeActual(observer: SingleObserver<in Instance>) {
|
||||||
val instance = instanceResponseCallback?.invoke()
|
val instance = instanceResponseCallback?.invoke()
|
||||||
|
@ -119,15 +98,27 @@ class ComposeActivityTest {
|
||||||
})
|
})
|
||||||
|
|
||||||
val instanceDaoMock = mock(InstanceDao::class.java)
|
val instanceDaoMock = mock(InstanceDao::class.java)
|
||||||
|
`when`(instanceDaoMock.loadMetadataForInstance(any())).thenReturn(
|
||||||
|
Single.just(InstanceEntity(instanceDomain, emptyList(),null, null, null, null))
|
||||||
|
)
|
||||||
|
|
||||||
val dbMock = mock(AppDatabase::class.java)
|
val dbMock = mock(AppDatabase::class.java)
|
||||||
`when`(dbMock.instanceDao()).thenReturn(instanceDaoMock)
|
`when`(dbMock.instanceDao()).thenReturn(instanceDaoMock)
|
||||||
|
|
||||||
activity.mastodonApi = apiMock
|
val viewModel = ComposeViewModel(
|
||||||
|
apiMock,
|
||||||
|
accountManagerMock,
|
||||||
|
mock(MediaUploader::class.java),
|
||||||
|
mock(ServiceClient::class.java),
|
||||||
|
mock(SaveTootHelper::class.java),
|
||||||
|
dbMock
|
||||||
|
)
|
||||||
|
|
||||||
|
val viewModelFactoryMock = mock(ViewModelFactory::class.java)
|
||||||
|
`when`(viewModelFactoryMock.create(ComposeViewModel::class.java)).thenReturn(viewModel)
|
||||||
|
|
||||||
activity.accountManager = accountManagerMock
|
activity.accountManager = accountManagerMock
|
||||||
activity.database = dbMock
|
activity.viewModelFactory = viewModelFactoryMock
|
||||||
|
|
||||||
`when`(accountManagerMock.activeAccount).thenReturn(account)
|
|
||||||
|
|
||||||
|
|
||||||
controller.create().start()
|
controller.create().start()
|
||||||
}
|
}
|
||||||
|
@ -164,7 +155,7 @@ class ComposeActivityTest {
|
||||||
fun whenMaximumTootCharsIsNull_defaultLimitIsUsed() {
|
fun whenMaximumTootCharsIsNull_defaultLimitIsUsed() {
|
||||||
instanceResponseCallback = { getInstanceWithMaximumTootCharacters(null) }
|
instanceResponseCallback = { getInstanceWithMaximumTootCharacters(null) }
|
||||||
setupActivity()
|
setupActivity()
|
||||||
assertEquals(ComposeActivity.STATUS_CHARACTER_LIMIT, activity.maximumTootCharacters)
|
assertEquals(DEFAULT_CHARACTER_LIMIT, activity.maximumTootCharacters)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -196,7 +187,7 @@ class ComposeActivityTest {
|
||||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||||
val additionalContent = " Check out this @image #search result: "
|
val additionalContent = " Check out this @image #search result: "
|
||||||
insertSomeTextInContent(shortUrl + additionalContent + url)
|
insertSomeTextInContent(shortUrl + additionalContent + url)
|
||||||
Assert.assertEquals(activity.calculateTextLength(), additionalContent.length + shortUrl.length + ComposeActivity.MAXIMUM_URL_LENGTH)
|
assertEquals(activity.calculateTextLength(), additionalContent.length + shortUrl.length + ComposeActivity.MAXIMUM_URL_LENGTH)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -204,7 +195,7 @@ class ComposeActivityTest {
|
||||||
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:"
|
||||||
val additionalContent = " Check out this @image #search result: "
|
val additionalContent = " Check out this @image #search result: "
|
||||||
insertSomeTextInContent(url + additionalContent + url)
|
insertSomeTextInContent(url + additionalContent + url)
|
||||||
Assert.assertEquals(activity.calculateTextLength(), additionalContent.length + (ComposeActivity.MAXIMUM_URL_LENGTH * 2))
|
assertEquals(activity.calculateTextLength(), additionalContent.length + (ComposeActivity.MAXIMUM_URL_LENGTH * 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clickUp() {
|
private fun clickUp() {
|
||||||
|
@ -256,13 +247,5 @@ class ComposeActivityTest {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getSuccessResponseCallbackWithMaximumTootCharacters(maximumTootCharacters: Int?): (Call<Instance>?, Callback<Instance>?) -> Unit
|
}
|
||||||
{
|
|
||||||
return {
|
|
||||||
call: Call<Instance>?, callback: Callback<Instance>? ->
|
|
||||||
if (call != null) {
|
|
||||||
callback?.onResponse(call, Response.success(getInstanceWithMaximumTootCharacters(maximumTootCharacters)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
package com.keylesspalace.tusky.util
|
||||||
|
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.junit.runners.Parameterized
|
||||||
|
|
||||||
|
@RunWith(Parameterized::class)
|
||||||
|
class VersionUtilsTest(
|
||||||
|
private val versionString: String,
|
||||||
|
private val supportsScheduledToots: Boolean
|
||||||
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
@Parameterized.Parameters
|
||||||
|
fun data() = listOf(
|
||||||
|
arrayOf("2.0.0", false),
|
||||||
|
arrayOf("2a9a0", false),
|
||||||
|
arrayOf("1.0", false),
|
||||||
|
arrayOf("error", false),
|
||||||
|
arrayOf("", false),
|
||||||
|
arrayOf("2.6.9", false),
|
||||||
|
arrayOf("2.7.0", true),
|
||||||
|
arrayOf("2.00008.0", true),
|
||||||
|
arrayOf("2.7.2 (compatible; Pleroma 1.0.0-1168-ge18c7866-pleroma-dot-site)", true),
|
||||||
|
arrayOf("3.0.1", true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testVersionUtils() {
|
||||||
|
assertEquals(VersionUtils(versionString).supportsScheduledToots(), supportsScheduledToots)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,14 +1,14 @@
|
||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.3.50'
|
ext.kotlin_version = '1.3.61'
|
||||||
repositories {
|
repositories {
|
||||||
jcenter()
|
jcenter()
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta07'
|
classpath 'com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta08'
|
||||||
classpath 'com.android.tools.build:gradle:3.5.2'
|
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
670/5000
|
|
||||||
Tusky v6.0
|
Tusky v6.0
|
||||||
|
|
||||||
- Els filtres de línia de temps s'han canviat a Preferències del compte i es sincronitzaran amb el servidor
|
- Els filtres de línia de temps s'han canviat a Preferències del compte i es sincronitzaran amb el servidor
|
||||||
- Ara podeu tenir un hashtag personalitzat com a pestanya a la interfície principal
|
- Ara podeu tenir un hashtag personalitzat com a pestanya a la interfície principal
|
||||||
- Ara es poden editar llistes
|
- Ara es poden editar llistes
|
||||||
- Seguretat: es va suprimir el suport per a TLS 1.0 i TLS 1.1, i es va afegir suport per a TLS 1.3 a Android 6+
|
- Seguretat: es va suprimir el suport per a TLS 1.0 i TLS 1.1 i es va afegir suport per a TLS 1.3 a Android 6+
|
||||||
- La vista de redacció ara suggerirà emojis personalitzats en començar a escriure
|
- La vista de redacció ara suggerirà emojis personalitzats en començar a escriure
|
||||||
- Configuració nova del tema "seguir el tema del sistema"
|
- Configuració nova del tema "seguir el tema del sistema"
|
||||||
- Mil
|
- Millora de l’
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
Tusky v9.0
|
||||||
|
|
||||||
|
- Ara podeu crear enquestes a partir de Tusky
|
||||||
|
- Millora de la cerca
|
||||||
|
- Nova opció a Preferències del compte per ampliar sempre els avisos de contingut
|
||||||
|
- Els avatars del calaix de navegació tenen ara una forma quadrada arrodonida
|
||||||
|
- Ara és possible informar als usuaris fins i tot quan mai no han publicat un estat
|
||||||
|
- Ara Tusky es negarà a connectar-se a connexions de text clar a Android 6+
|
||||||
|
- Un munt d’altres petites millores i solucions d’errors
|
|
@ -0,0 +1,3 @@
|
||||||
|
Tusky v9.1
|
||||||
|
|
||||||
|
Aquesta versió garanteix la compatibilitat amb Mastodon 3 i millora el rendiment i l'estabilitat.
|
|
@ -0,0 +1,8 @@
|
||||||
|
Tusky v6.0
|
||||||
|
|
||||||
|
- I filtri della timeline sono stati spostati in Preferenze Utente e si sincronizzeranno con il server
|
||||||
|
- Ora è possibile avere un hashtag personalizzato come scheda nell'interfaccia principale
|
||||||
|
- Le liste possono ora essere modificate
|
||||||
|
- Sicurezza: rimosso il supporto per TLS 1.0 e TLS 1.1.1, e aggiunto il supporto per TLS 1.3 su Android 6+.
|
||||||
|
- La vista della composizione suggerirà ora le emojis personalizzate quando si inizia a digitare
|
||||||
|
- Nuova impostazione del tema "Segui il tema del
|
|
@ -0,0 +1,7 @@
|
||||||
|
Tusky v7.0
|
||||||
|
|
||||||
|
- Supporto per la visualizzazione di sondaggi, voti e relative notifiche
|
||||||
|
- Nuovi bottoni per filtrare le notifiche ed eliminarle tutte
|
||||||
|
- Cancella e riscrivi i tuoi toots
|
||||||
|
- Nuovo indicatore che mostra sull'immagine del profilo se un account è un bot (può essere disattivato nelle preferenze)
|
||||||
|
- Nuove traduzioni: Norvegese Bokmål e sloveno.
|
|
@ -0,0 +1,9 @@
|
||||||
|
Tusky v9.0
|
||||||
|
|
||||||
|
- Si possono creare sondaggi da Tusky
|
||||||
|
- Ricerca migliorata
|
||||||
|
- Nuova opzione nelle preferenze utente per espandere sempre i contenuti sensibili
|
||||||
|
- Le icone di navigazione hanno ora una forma quadrata arrotondata
|
||||||
|
- È ora possibile segnalare gli utenti anche prima che pubblichino nulla
|
||||||
|
- Tusky ora si rifiuterà di connettersi attraverso connessioni non cifrate su Android 6+
|
||||||
|
- Molti altri piccoli miglioramenti e correzioni di errori
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue