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.okhttpVersion = '4.2.2'
|
||||
ext.glideVersion = '4.10.0'
|
||||
ext.daggerVersion = '2.25.2'
|
||||
ext.daggerVersion = '2.25.3'
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
|
@ -116,26 +117,28 @@ dependencies {
|
|||
|
||||
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.fragment:fragment-ktx:1.1.0"
|
||||
implementation "androidx.browser:browser:1.0.0"
|
||||
implementation "androidx.recyclerview:recyclerview:1.0.0"
|
||||
implementation "androidx.exifinterface:exifinterface:1.0.0"
|
||||
implementation "androidx.browser:browser:1.2.0"
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout: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.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-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.paging:paging-runtime-ktx:2.1.0"
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0-rc01"
|
||||
implementation "androidx.paging:paging-runtime-ktx:2.1.1"
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
implementation "androidx.room:room-runtime:$roomVersion"
|
||||
implementation "androidx.room:room-rxjava2:$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:converter-gson:$retrofitVersion"
|
||||
|
@ -149,7 +152,7 @@ dependencies {
|
|||
implementation "com.github.bumptech.glide:glide:$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:rxkotlin:2.4.0"
|
||||
|
||||
|
@ -162,7 +165,7 @@ dependencies {
|
|||
implementation "com.google.dagger:dagger-android-support:$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"
|
||||
|
||||
|
@ -182,7 +185,7 @@ dependencies {
|
|||
|
||||
testImplementation "androidx.test.ext:junit:1.1.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"
|
||||
|
||||
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
|
||||
android:name=".ComposeActivity"
|
||||
android:name=".components.compose.ComposeActivity"
|
||||
android:theme="@style/TuskyDialogActivityTheme"
|
||||
android:windowSoftInputMode="stateVisible|adjustResize"/>
|
||||
<activity
|
||||
|
|
|
@ -19,8 +19,7 @@ import android.animation.ArgbEvaluator
|
|||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.*
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
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.TabLayoutMediator
|
||||
import com.keylesspalace.tusky.adapter.AccountFieldAdapter
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||
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.interfaces.ActionButtonActivity
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
|
@ -117,7 +119,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
viewModel = ViewModelProviders.of(this, viewModelFactory)[AccountViewModel::class.java]
|
||||
|
||||
// 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)
|
||||
animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false)
|
||||
|
@ -265,7 +267,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
|
||||
|
||||
if(verticalOffset == oldOffset) {
|
||||
if (verticalOffset == oldOffset) {
|
||||
return
|
||||
}
|
||||
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)
|
||||
LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this, false)
|
||||
|
||||
accountFieldAdapter.fields = account.fields ?: emptyList()
|
||||
// accountFieldAdapter.fields = account.fields ?: emptyList()
|
||||
accountFieldAdapter.emojis = account.emojis ?: emptyList()
|
||||
accountFieldAdapter.notifyDataSetChanged()
|
||||
|
||||
|
@ -471,7 +478,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
// this is necessary because API 19 can't handle vector compound drawables
|
||||
val movedIcon = ContextCompat.getDrawable(this, R.drawable.ic_briefcase)?.mutate()
|
||||
val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary)
|
||||
movedIcon?.setColorFilter(textColor, PorterDuff.Mode.SRC_IN)
|
||||
movedIcon?.colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN)
|
||||
|
||||
accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null)
|
||||
}
|
||||
|
@ -693,9 +700,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
|
||||
private fun mention() {
|
||||
loadedAccount?.let {
|
||||
val intent = ComposeActivity.IntentBuilder()
|
||||
.mentionedUsernames(setOf(it.username))
|
||||
.build(this)
|
||||
val intent = ComposeActivity.startIntent(this,
|
||||
ComposeActivity.ComposeOptions(mentionedUsernames = setOf(it.username)))
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
@ -754,7 +760,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
|
|||
return true
|
||||
}
|
||||
R.id.action_report -> {
|
||||
if(loadedAccount != null) {
|
||||
if (loadedAccount != null) {
|
||||
startActivity(ReportActivity.getIntent(this, viewModel.accountId, loadedAccount!!.username))
|
||||
}
|
||||
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.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.browser.customtabs.CustomTabsIntent
|
||||
import com.bumptech.glide.Glide
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.AccessToken
|
||||
|
@ -338,9 +339,16 @@ class LoginActivity : BaseActivity(), Injectable {
|
|||
private fun openInCustomTab(uri: Uri, context: Context): Boolean {
|
||||
|
||||
val toolbarColor = ThemeUtils.getColor(context, R.attr.custom_tab_toolbar)
|
||||
val customTabsIntent = CustomTabsIntent.Builder()
|
||||
val customTabsIntentBuilder = CustomTabsIntent.Builder()
|
||||
.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 {
|
||||
customTabsIntent.launchUrl(context, uri)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
|
|
|
@ -52,6 +52,7 @@ import com.keylesspalace.tusky.appstore.DrawerFooterClickedEvent;
|
|||
import com.keylesspalace.tusky.appstore.EventHub;
|
||||
import com.keylesspalace.tusky.appstore.MainTabsChangedEvent;
|
||||
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.search.SearchActivity;
|
||||
import com.keylesspalace.tusky.db.AccountEntity;
|
||||
|
|
|
@ -22,21 +22,6 @@ import android.view.MenuItem;
|
|||
import android.view.View;
|
||||
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.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
|
@ -44,16 +29,35 @@ import androidx.lifecycle.Lifecycle;
|
|||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
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 static com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions;
|
||||
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
||||
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
||||
|
||||
public final class SavedTootActivity extends BaseActivity implements SavedTootAdapter.SavedTootAction,
|
||||
Injectable {
|
||||
|
||||
private SaveTootHelper saveTootHelper;
|
||||
|
||||
// ui
|
||||
private SavedTootAdapter adapter;
|
||||
private TextView noContent;
|
||||
|
@ -66,13 +70,13 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
|
|||
EventHub eventHub;
|
||||
@Inject
|
||||
AppDatabase database;
|
||||
@Inject
|
||||
SaveTootHelper saveTootHelper;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
saveTootHelper = new SaveTootHelper(database.tootDao(), this);
|
||||
|
||||
eventHub.getEvents()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.ofType(StatusComposedEvent.class)
|
||||
|
@ -153,18 +157,32 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
|
|||
|
||||
@Override
|
||||
public void click(int position, TootEntity item) {
|
||||
Intent intent = new ComposeActivity.IntentBuilder()
|
||||
.savedTootUid(item.getUid())
|
||||
.tootText(item.getText())
|
||||
.contentWarning(item.getContentWarning())
|
||||
.savedJsonUrls(item.getUrls())
|
||||
.savedJsonDescriptions(item.getDescriptions())
|
||||
.inReplyToId(item.getInReplyToId())
|
||||
.replyingStatusAuthor(item.getInReplyToUsername())
|
||||
.replyingStatusContent(item.getInReplyToText())
|
||||
.visibility(item.getVisibility())
|
||||
.poll(item.getPoll())
|
||||
.build(this);
|
||||
Gson gson = new Gson();
|
||||
Type stringListType = new TypeToken<List<String>>() {}.getType();
|
||||
List<String> jsonUrls = gson.fromJson(item.getUrls(), stringListType);
|
||||
List<String> descriptions = gson.fromJson(item.getDescriptions(), stringListType);
|
||||
|
||||
ComposeOptions composeOptions = new ComposeOptions(
|
||||
item.getUid(),
|
||||
item.getText(),
|
||||
jsonUrls,
|
||||
descriptions,
|
||||
/*mentionedUsernames*/null,
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
|
@ -11,6 +12,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import com.keylesspalace.tusky.adapter.ScheduledTootAdapter
|
||||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.di.Injectable
|
||||
import com.keylesspalace.tusky.entity.ScheduledStatus
|
||||
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() {
|
||||
progress_bar.visibility = View.VISIBLE
|
||||
mastodonApi.scheduledStatuses()
|
||||
|
@ -135,15 +147,15 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootAdapter.ScheduledToot
|
|||
if (item == null) {
|
||||
return
|
||||
}
|
||||
val intent = ComposeActivity.IntentBuilder()
|
||||
.tootText(item.params.text)
|
||||
.contentWarning(item.params.spoilerText)
|
||||
.mediaAttachments(item.mediaAttachments)
|
||||
.inReplyToId(item.params.inReplyToId)
|
||||
.visibility(item.params.visibility)
|
||||
.scheduledAt(item.scheduledAt)
|
||||
.sensitive(item.params.sensitive)
|
||||
.build(this)
|
||||
val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions(
|
||||
tootText = item.params.text,
|
||||
contentWarning = item.params.spoilerText,
|
||||
mediaAttachments = item.mediaAttachments,
|
||||
inReplyToId = item.params.inReplyToId,
|
||||
visibility = item.params.visibility,
|
||||
scheduledAt = item.scheduledAt,
|
||||
sensitive = item.params.sensitive
|
||||
))
|
||||
startActivity(intent)
|
||||
delete(position, item)
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers
|
|||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.android.synthetic.main.activity_tab_preference.*
|
||||
import kotlinx.android.synthetic.main.toolbar_basic.*
|
||||
import kotlinx.android.synthetic.main.item_tab_preference.view.removeButton
|
||||
import java.util.regex.Pattern
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -76,7 +77,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
}
|
||||
|
||||
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.layoutManager = LinearLayoutManager(this)
|
||||
currentTabsRecyclerView.addItemDecoration(DividerItemDecoration(this, LinearLayoutManager.VERTICAL))
|
||||
|
@ -109,10 +110,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
}
|
||||
|
||||
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
|
||||
currentTabs.removeAt(viewHolder.adapterPosition)
|
||||
currentTabsAdapter.notifyItemRemoved(viewHolder.adapterPosition)
|
||||
updateAvailableTabs()
|
||||
saveTabs()
|
||||
onTabRemoved(viewHolder.adapterPosition)
|
||||
}
|
||||
|
||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
|
@ -168,6 +166,13 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
saveTabs()
|
||||
}
|
||||
|
||||
override fun onTabRemoved(position: Int) {
|
||||
currentTabs.removeAt(position)
|
||||
currentTabsAdapter.notifyItemRemoved(position)
|
||||
updateAvailableTabs()
|
||||
saveTabs()
|
||||
}
|
||||
|
||||
override fun onActionChipClicked(tab: TabData) {
|
||||
showEditHashtagDialog(tab)
|
||||
}
|
||||
|
@ -273,7 +278,7 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene
|
|||
addTabAdapter.updateData(addableTabs)
|
||||
|
||||
maxTabsInfo.visible(addableTabs.size == 0 || currentTabs.size >= MAX_TAB_COUNT)
|
||||
|
||||
currentTabsAdapter.setRemoveButtonVisible(currentTabs.size > MIN_TAB_COUNT);
|
||||
}
|
||||
|
||||
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_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
|
||||
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();
|
||||
accountManager = new AccountManager(appDatabase);
|
||||
serviceLocator = new ServiceLocator() {
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky.adapter
|
||||
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
@ -23,15 +24,17 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.Field
|
||||
import com.keylesspalace.tusky.entity.IdentityProof
|
||||
import com.keylesspalace.tusky.interfaces.LinkListener
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.LinkHelper
|
||||
import kotlinx.android.synthetic.main.item_account_field.view.*
|
||||
|
||||
class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView.Adapter<AccountFieldAdapter.ViewHolder>() {
|
||||
|
||||
var emojis: List<Emoji> = emptyList()
|
||||
var fields: List<Field> = emptyList()
|
||||
var fields: List<Either<IdentityProof, Field>> = emptyList()
|
||||
|
||||
override fun getItemCount() = fields.size
|
||||
|
||||
|
@ -41,18 +44,30 @@ class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView
|
|||
}
|
||||
|
||||
override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) {
|
||||
val field = fields[position]
|
||||
val proofOrField = fields[position]
|
||||
|
||||
val emojifiedName = CustomEmojiHelper.emojifyString(field.name, emojis, viewHolder.nameTextView)
|
||||
viewHolder.nameTextView.text = emojifiedName
|
||||
if(proofOrField.isLeft()) {
|
||||
val identityProof = proofOrField.asLeft()
|
||||
|
||||
val emojifiedValue = CustomEmojiHelper.emojifyText(field.value, emojis, viewHolder.valueTextView)
|
||||
LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener, false)
|
||||
viewHolder.nameTextView.text = identityProof.provider
|
||||
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)
|
||||
} 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 Status.Mention[] mentions,
|
||||
@NonNull List<Emoji> emojis,
|
||||
@Nullable PollViewData poll,
|
||||
final StatusActionListener listener,
|
||||
boolean removeQuote) {
|
||||
if (TextUtils.isEmpty(spoilerText)) {
|
||||
contentWarningDescription.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 {
|
||||
CharSequence emojiSpoiler = CustomEmojiHelper.emojifyString(spoilerText, emojis, contentWarningDescription);
|
||||
contentWarningDescription.setText(emojiSpoiler);
|
||||
|
@ -206,9 +207,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||
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,
|
||||
Status.Mention[] mentions,
|
||||
List<Emoji> emojis,
|
||||
@Nullable PollViewData poll,
|
||||
final StatusActionListener listener,
|
||||
boolean removeQuote) {
|
||||
if (expanded) {
|
||||
Spanned emojifiedText = CustomEmojiHelper.emojifyText(content, emojis, this.content);
|
||||
LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener, removeQuote);
|
||||
if (poll != null) {
|
||||
setupPoll(poll, emojis, listener);
|
||||
}
|
||||
} else {
|
||||
LinkHelper.setClickableMentions(this.content, mentions, listener);
|
||||
}
|
||||
|
@ -229,6 +234,14 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
} else {
|
||||
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,
|
||||
|
@ -663,40 +676,18 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
replyButton.setEnabled(!isNotestock);
|
||||
replyButton.setClickable(!isNotestock);
|
||||
if (reblogButton != null) {
|
||||
reblogButton.setEventListener(new SparkEventListener() {
|
||||
@Override
|
||||
public void onEvent(ImageView button, boolean buttonState) {
|
||||
int position = getAdapterPosition();
|
||||
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) {
|
||||
reblogButton.setEventListener((button, buttonState) -> {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onReblog(buttonState, position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
favouriteButton.setEventListener(new SparkEventListener() {
|
||||
@Override
|
||||
public void onEvent(ImageView button, boolean buttonState) {
|
||||
int position = getAdapterPosition();
|
||||
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) {
|
||||
favouriteButton.setEventListener((button, buttonState) -> {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onFavourite(buttonState, position);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -712,23 +703,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
});
|
||||
}
|
||||
|
||||
bookmarkButton.setEventListener(new SparkEventListener() {
|
||||
@Override
|
||||
public void onEvent(ImageView button, boolean buttonState) {
|
||||
int position = getAdapterPosition();
|
||||
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) {
|
||||
|
||||
bookmarkButton.setEventListener((button, buttonState) -> {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onBookmark(buttonState, position);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -802,13 +780,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
|||
setRebloggingEnabled(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,
|
||||
status.getQuote() != null);
|
||||
setSpoilerAndContent(status.isExpanded(), status.getContent(), status.getSpoilerText(), status.getMentions(), status.getStatusEmojis(), status.getPoll(), listener, status.getQuote() != null);
|
||||
|
||||
setDescriptionForStatus(status);
|
||||
|
||||
setupPoll(status.getPoll(), status.getStatusEmojis(), listener);
|
||||
|
||||
// Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0
|
||||
// RecyclerView tries to set AccessibilityDelegateCompat to null
|
||||
// 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) {
|
||||
if (poll == null) {
|
||||
private void setupPoll(PollViewData poll, List<Emoji> emojis, StatusActionListener listener) {
|
||||
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);
|
||||
|
||||
} 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()) {
|
||||
// no voting possible
|
||||
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), emojis, PollAdapter.RESULT);
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
|
||||
pollButton.setVisibility(View.GONE);
|
||||
} else {
|
||||
// voting possible
|
||||
pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE);
|
||||
List<Integer> pollResult = pollAdapter.getSelected();
|
||||
|
||||
pollButton.setVisibility(View.VISIBLE);
|
||||
|
||||
pollButton.setOnClickListener(v -> {
|
||||
|
||||
int position = getAdapterPosition();
|
||||
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
|
||||
List<Integer> pollResult = pollAdapter.getSelected();
|
||||
|
||||
if (!pollResult.isEmpty()) {
|
||||
listener.onVoteInPoll(position, pollResult);
|
||||
}
|
||||
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) {
|
||||
|
|
|
@ -118,10 +118,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
|||
timestampInfo.append(" • ");
|
||||
|
||||
if (app.getWebsite() != null) {
|
||||
URLSpan span = new CustomURLSpan(app.getWebsite());
|
||||
|
||||
SpannableStringBuilder text = new SpannableStringBuilder(app.getName());
|
||||
text.setSpan(span, 0, app.getName().length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
CharSequence text = LinkHelper.createClickableText(app.getName(), app.getWebsite());
|
||||
timestampInfo.append(text);
|
||||
timestampInfo.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
} else {
|
||||
|
|
|
@ -32,14 +32,16 @@ import kotlinx.android.synthetic.main.item_tab_preference.view.*
|
|||
|
||||
interface ItemInteractionListener {
|
||||
fun onTabAdded(tab: TabData)
|
||||
fun onTabRemoved(position: Int)
|
||||
fun onStartDelete(viewHolder: RecyclerView.ViewHolder)
|
||||
fun onStartDrag(viewHolder: RecyclerView.ViewHolder)
|
||||
fun onActionChipClicked(tab: TabData)
|
||||
}
|
||||
|
||||
class TabAdapter(private var data: List<TabData>,
|
||||
private val small: Boolean = false,
|
||||
private val listener: ItemInteractionListener? = null) : RecyclerView.Adapter<TabAdapter.ViewHolder>() {
|
||||
private val small: Boolean,
|
||||
private val listener: ItemInteractionListener,
|
||||
private var removeButtonEnabled: Boolean = false) : RecyclerView.Adapter<TabAdapter.ViewHolder>() {
|
||||
|
||||
fun updateData(newData: List<TabData>) {
|
||||
this.data = newData
|
||||
|
@ -67,17 +69,28 @@ class TabAdapter(private var data: List<TabData>,
|
|||
holder.itemView.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(iconDrawable, null, null, null)
|
||||
if (small) {
|
||||
holder.itemView.textView.setOnClickListener {
|
||||
listener?.onTabAdded(data[position])
|
||||
listener.onTabAdded(data[position])
|
||||
}
|
||||
}
|
||||
holder.itemView.imageView?.setOnTouchListener { _, event ->
|
||||
if (event.action == MotionEvent.ACTION_DOWN) {
|
||||
listener?.onStartDrag(holder)
|
||||
listener.onStartDrag(holder)
|
||||
true
|
||||
} else {
|
||||
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) {
|
||||
|
||||
|
@ -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.setOnClickListener {
|
||||
listener?.onActionChipClicked(data[position])
|
||||
listener.onActionChipClicked(data[position])
|
||||
}
|
||||
|
||||
} else {
|
||||
|
@ -102,5 +115,12 @@ class TabAdapter(private var data: List<TabData>,
|
|||
return data.size
|
||||
}
|
||||
|
||||
fun setRemoveButtonVisible(enabled: Boolean) {
|
||||
if (removeButtonEnabled != enabled) {
|
||||
removeButtonEnabled = enabled
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.util;
|
||||
package com.keylesspalace.tusky.components.compose;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.graphics.Bitmap;
|
||||
|
@ -21,6 +21,8 @@ import android.graphics.BitmapFactory;
|
|||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import com.keylesspalace.tusky.util.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
|
@ -42,10 +44,10 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
|||
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 tempFile the file where the result will be stored
|
||||
* @param listener to whom the results are given
|
||||
* @param tempFile the file where the result will be stored
|
||||
* @param listener to whom the results are given
|
||||
*/
|
||||
public DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, File tempFile, Listener listener) {
|
||||
this.sizeLimit = sizeLimit;
|
||||
|
@ -56,6 +58,25 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
|||
|
||||
@Override
|
||||
protected Boolean doInBackground(Uri... uris) {
|
||||
boolean result = DownsizeImageTask.resize(uris, sizeLimit, contentResolver, tempFile);
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean successful) {
|
||||
if (successful) {
|
||||
listener.onSuccess(tempFile);
|
||||
} else {
|
||||
listener.onFailure();
|
||||
}
|
||||
super.onPostExecute(successful);
|
||||
}
|
||||
|
||||
public static boolean resize(Uri[] uris, int sizeLimit, ContentResolver contentResolver,
|
||||
File tempFile) {
|
||||
for (Uri uri : uris) {
|
||||
InputStream inputStream;
|
||||
try {
|
||||
|
@ -118,27 +139,16 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
|||
reorientedBitmap.recycle();
|
||||
scaledImageSize /= 2;
|
||||
} while (tempFile.length() > sizeLimit);
|
||||
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean successful) {
|
||||
if (successful) {
|
||||
listener.onSuccess(tempFile);
|
||||
} else {
|
||||
listener.onFailure();
|
||||
}
|
||||
super.onPostExecute(successful);
|
||||
}
|
||||
|
||||
/** Used to communicate the results of the task. */
|
||||
/**
|
||||
* Used to communicate the results of the task.
|
||||
*/
|
||||
public interface Listener {
|
||||
void onSuccess(File file);
|
||||
|
||||
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")
|
||||
|
||||
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 com.keylesspalace.tusky.ComposeActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.adapter.AddPollOptionsAdapter
|
||||
import com.keylesspalace.tusky.entity.NewPoll
|
||||
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(
|
||||
activity: ComposeActivity,
|
||||
context: Context,
|
||||
poll: NewPoll?,
|
||||
maxOptionCount: Int?,
|
||||
maxOptionLength: Int?
|
||||
maxOptionCount: 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)
|
||||
.setTitle(R.string.create_poll_title)
|
||||
.setView(view)
|
||||
|
@ -47,7 +46,7 @@ fun showAddPollDialog(
|
|||
|
||||
val adapter = AddPollOptionsAdapter(
|
||||
options = poll?.options?.toMutableList() ?: mutableListOf("", ""),
|
||||
maxOptionLength = maxOptionLength ?: DEFAULT_MAX_OPTION_LENGTH,
|
||||
maxOptionLength = maxOptionLength,
|
||||
onOptionRemoved = { valid ->
|
||||
view.addChoiceButton.isEnabled = true
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid
|
||||
|
@ -60,15 +59,15 @@ fun showAddPollDialog(
|
|||
view.pollChoices.adapter = adapter
|
||||
|
||||
view.addChoiceButton.setOnClickListener {
|
||||
if (adapter.itemCount < maxOptionCount ?: DEFAULT_MAX_OPTION_COUNT) {
|
||||
if (adapter.itemCount < maxOptionCount) {
|
||||
adapter.addChoice()
|
||||
}
|
||||
if (adapter.itemCount >= maxOptionCount ?: DEFAULT_MAX_OPTION_COUNT) {
|
||||
if (adapter.itemCount >= maxOptionCount) {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -81,15 +80,14 @@ fun showAddPollDialog(
|
|||
button.setOnClickListener {
|
||||
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(
|
||||
NewPoll(
|
||||
options = adapter.pollOptions,
|
||||
expiresIn = pollDuration,
|
||||
multiple = view.multipleChoicesCheckBox.isChecked
|
||||
)
|
||||
)
|
||||
onUpdatePoll(NewPoll(
|
||||
options = adapter.pollOptions,
|
||||
expiresIn = pollDuration,
|
||||
multiple = view.multipleChoicesCheckBox.isChecked
|
||||
))
|
||||
|
||||
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,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.view
|
||||
package com.keylesspalace.tusky.components.compose.view
|
||||
|
||||
import android.content.Context
|
||||
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,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.view;
|
||||
package com.keylesspalace.tusky.components.compose.view;
|
||||
|
||||
import android.content.Context;
|
||||
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.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.fragment.TimePickerFragment;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
|
@ -87,7 +88,7 @@ public class ComposeScheduleView extends ConstraintLayout {
|
|||
|
||||
private void setScheduledDateTime() {
|
||||
if (scheduleDateTime == null) {
|
||||
scheduledDateTimeView.setText(R.string.hint_configure_scheduled_toot);
|
||||
scheduledDateTimeView.setText("");
|
||||
} else {
|
||||
scheduledDateTimeView.setText(String.format("%s %s",
|
||||
dateFormat.format(scheduleDateTime.getTime()),
|
||||
|
@ -96,13 +97,13 @@ public class ComposeScheduleView extends ConstraintLayout {
|
|||
}
|
||||
|
||||
private void setEditIcons() {
|
||||
final int size = scheduledDateTimeView.getLineHeight();
|
||||
|
||||
Drawable icon = getContext().getDrawable(R.drawable.ic_create_24dp);
|
||||
Drawable icon = ThemeUtils.getTintedDrawable(getContext(), R.drawable.ic_create_24dp, android.R.attr.textColorTertiary);
|
||||
if (icon == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int size = scheduledDateTimeView.getLineHeight();
|
||||
|
||||
icon.setBounds(0, 0, size, size);
|
||||
|
||||
scheduledDateTimeView.setCompoundDrawables(null, null, icon, null);
|
||||
|
@ -117,7 +118,7 @@ public class ComposeScheduleView extends ConstraintLayout {
|
|||
setScheduledDateTime();
|
||||
}
|
||||
|
||||
private void openPickDateDialog() {
|
||||
public void openPickDateDialog() {
|
||||
long yesterday = Calendar.getInstance().getTimeInMillis() - 24 * 60 * 60 * 1000;
|
||||
CalendarConstraints calendarConstraints = new CalendarConstraints.Builder()
|
||||
.setValidator(
|
|
@ -13,7 +13,7 @@
|
|||
* 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.view
|
||||
package com.keylesspalace.tusky.components.compose.view
|
||||
|
||||
import android.content.Context
|
||||
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,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.view
|
||||
package com.keylesspalace.tusky.components.compose.view
|
||||
|
||||
import android.content.Context
|
||||
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,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.view;
|
||||
package com.keylesspalace.tusky.components.compose.view;
|
||||
|
||||
import android.content.Context;
|
||||
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,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.view
|
||||
package com.keylesspalace.tusky.components.compose.view
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
|
@ -108,14 +108,11 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
|
|||
|
||||
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());
|
||||
|
||||
setAvatars(conversation.getAccounts());
|
||||
|
||||
setupPoll(PollViewDataKt.toViewData(status.getPoll()), status.getEmojis(), listener);
|
||||
|
||||
}
|
||||
|
||||
private void setConversationName(List<ConversationAccountEntity> accounts) {
|
||||
|
|
|
@ -38,7 +38,12 @@ import androidx.paging.PagedListAdapter
|
|||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
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.search.adapter.SearchStatusesAdapter
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
|
@ -201,14 +206,14 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
|
|||
mentionedUsernames.add(username)
|
||||
}
|
||||
mentionedUsernames.remove(loggedInUsername)
|
||||
val intent = ComposeActivity.IntentBuilder()
|
||||
.inReplyToId(inReplyToId)
|
||||
.replyVisibility(replyVisibility)
|
||||
.contentWarning(contentWarning)
|
||||
.mentionedUsernames(mentionedUsernames)
|
||||
.replyingStatusAuthor(actionableStatus.account.localUsername)
|
||||
.replyingStatusContent(actionableStatus.content.toString())
|
||||
.build(context)
|
||||
val intent = ComposeActivity.startIntent(context!!, ComposeOptions(
|
||||
inReplyToId = inReplyToId,
|
||||
replyVisibility = replyVisibility,
|
||||
contentWarning = contentWarning,
|
||||
mentionedUsernames = mentionedUsernames,
|
||||
replyingStatusAuthor = actionableStatus.account.localUsername,
|
||||
replyingStatusContent = actionableStatus.content.toString()
|
||||
))
|
||||
requireActivity().startActivity(intent)
|
||||
}
|
||||
|
||||
|
@ -225,12 +230,12 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
|
|||
mentionedUsernames.add(username)
|
||||
}
|
||||
mentionedUsernames.remove(loggedInUsername)
|
||||
val intent = ComposeActivity.IntentBuilder()
|
||||
.quoteId(id)
|
||||
.quoteUrl(url)
|
||||
.replyVisibility(visibility)
|
||||
.mentionedUsernames(mentionedUsernames)
|
||||
.build(context)
|
||||
val intent = ComposeActivity.startIntent(context!!, ComposeOptions(
|
||||
quoteId = id,
|
||||
quoteUrl = url,
|
||||
replyVisibility = visibility,
|
||||
mentionedUsernames = mentionedUsernames
|
||||
))
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
|
@ -426,24 +431,24 @@ open class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.C
|
|||
viewModel.deleteStatus(id)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.autoDispose(from(this, Lifecycle.Event.ON_DESTROY))
|
||||
.subscribe ({ deletedStatus ->
|
||||
.subscribe({ deletedStatus ->
|
||||
removeItem(position)
|
||||
|
||||
val redraftStatus = if(deletedStatus.isEmpty()) {
|
||||
val redraftStatus = if (deletedStatus.isEmpty()) {
|
||||
status.toDeletedStatus()
|
||||
} else {
|
||||
deletedStatus
|
||||
}
|
||||
|
||||
val intent = ComposeActivity.IntentBuilder()
|
||||
.tootText(redraftStatus.text)
|
||||
.inReplyToId(redraftStatus.inReplyToId)
|
||||
.visibility(redraftStatus.visibility)
|
||||
.contentWarning(redraftStatus.spoilerText)
|
||||
.mediaAttachments(redraftStatus.attachments)
|
||||
.sensitive(redraftStatus.sensitive)
|
||||
.poll(redraftStatus.poll?.toNewPoll(status.createdAt))
|
||||
.build(context)
|
||||
val intent = ComposeActivity.startIntent(context!!, ComposeOptions(
|
||||
tootText = redraftStatus.text ?: "",
|
||||
inReplyToId = redraftStatus.inReplyToId,
|
||||
visibility = redraftStatus.visibility,
|
||||
contentWarning = redraftStatus.spoilerText,
|
||||
mediaAttachments = redraftStatus.attachments,
|
||||
sensitive = redraftStatus.sensitive,
|
||||
poll = redraftStatus.poll?.toNewPoll(status.createdAt)
|
||||
))
|
||||
startActivity(intent)
|
||||
}, { 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,
|
||||
TimelineAccountEntity.class, ConversationEntity.class
|
||||
}, version = 20)
|
||||
}, version = 21)
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
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 `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.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import io.reactivex.Single
|
||||
|
||||
@Dao
|
||||
interface InstanceDao {
|
||||
|
@ -26,5 +27,5 @@ interface InstanceDao {
|
|||
fun insertOrReplace(instance: InstanceEntity)
|
||||
|
||||
@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 maximumTootCharacters: Int?,
|
||||
val maxPollOptions: Int?,
|
||||
val maxPollOptionLength: Int?
|
||||
val maxPollOptionLength: Int?,
|
||||
val version: String?
|
||||
)
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
package com.keylesspalace.tusky.di
|
||||
|
||||
import com.keylesspalace.tusky.*
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||
|
|
|
@ -35,7 +35,8 @@ import javax.inject.Singleton
|
|||
ServicesModule::class,
|
||||
BroadcastReceiverModule::class,
|
||||
ViewModelModule::class,
|
||||
RepositoryModule::class
|
||||
RepositoryModule::class,
|
||||
MediaUploaderModule::class
|
||||
])
|
||||
interface AppComponent {
|
||||
@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
|
||||
|
||||
import android.content.Context
|
||||
import com.keylesspalace.tusky.service.SendTootService
|
||||
import com.keylesspalace.tusky.service.ServiceClient
|
||||
import com.keylesspalace.tusky.service.ServiceClientImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.android.ContributesAndroidInjector
|
||||
|
||||
@Module
|
||||
abstract class ServicesModule {
|
||||
@ContributesAndroidInjector
|
||||
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.ViewModelProvider
|
||||
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||
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 dagger.Binds
|
||||
import dagger.MapKey
|
||||
|
@ -71,5 +74,10 @@ abstract class ViewModelModule {
|
|||
@ViewModelKey(SearchViewModel::class)
|
||||
internal abstract fun searchViewModel(viewModel: SearchViewModel): ViewModel
|
||||
|
||||
@Binds
|
||||
@IntoMap
|
||||
@ViewModelKey(ComposeViewModel::class)
|
||||
internal abstract fun composeViewModel(viewModel: ComposeViewModel): ViewModel
|
||||
|
||||
//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.BottomSheetActivity;
|
||||
import com.keylesspalace.tusky.ComposeActivity;
|
||||
import com.keylesspalace.tusky.MainActivity;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.PostLookupFallbackBehavior;
|
||||
import com.keylesspalace.tusky.ViewMediaActivity;
|
||||
import com.keylesspalace.tusky.ViewTagActivity;
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions;
|
||||
import com.keylesspalace.tusky.components.report.ReportActivity;
|
||||
import com.keylesspalace.tusky.db.AccountEntity;
|
||||
import com.keylesspalace.tusky.db.AccountManager;
|
||||
|
@ -148,21 +149,22 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
|||
mentionedUsernames.add(actionableStatus.getAccount().getUsername());
|
||||
String loggedInUsername = null;
|
||||
AccountEntity activeAccount = accountManager.getActiveAccount();
|
||||
if(activeAccount != null) {
|
||||
if (activeAccount != null) {
|
||||
loggedInUsername = activeAccount.getUsername();
|
||||
}
|
||||
for (Status.Mention mention : mentions) {
|
||||
mentionedUsernames.add(mention.getUsername());
|
||||
}
|
||||
mentionedUsernames.remove(loggedInUsername);
|
||||
Intent intent = new ComposeActivity.IntentBuilder()
|
||||
.inReplyToId(inReplyToId)
|
||||
.replyVisibility(replyVisibility)
|
||||
.contentWarning(contentWarning)
|
||||
.mentionedUsernames(mentionedUsernames)
|
||||
.replyingStatusAuthor(actionableStatus.getAccount().getLocalUsername())
|
||||
.replyingStatusContent(actionableStatus.getContent().toString())
|
||||
.build(getContext());
|
||||
ComposeOptions composeOptions = new ComposeOptions();
|
||||
composeOptions.setInReplyToId(inReplyToId);
|
||||
composeOptions.setReplyVisibility(replyVisibility);
|
||||
composeOptions.setContentWarning(contentWarning);
|
||||
composeOptions.setMentionedUsernames(mentionedUsernames);
|
||||
composeOptions.setReplyingStatusAuthor(actionableStatus.getAccount().getLocalUsername());
|
||||
composeOptions.setReplyingStatusContent(actionableStatus.getContent().toString());
|
||||
|
||||
Intent intent = ComposeActivity.startIntent(getContext(), composeOptions);
|
||||
getActivity().startActivity(intent);
|
||||
}
|
||||
|
||||
|
@ -186,12 +188,13 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
|||
if (status.getReblog() != null) {
|
||||
url = status.getReblog().getUrl();
|
||||
}
|
||||
Intent intent = new ComposeActivity.IntentBuilder()
|
||||
.quoteId(id)
|
||||
.quoteUrl(url)
|
||||
.replyVisibility(visibility)
|
||||
.mentionedUsernames(mentionedUsernames)
|
||||
.build(getContext());
|
||||
ComposeOptions composeOptions = new ComposeOptions();
|
||||
composeOptions.setQuoteId(id);
|
||||
composeOptions.setQuoteUrl(url);
|
||||
composeOptions.setReplyVisibility(visibility);
|
||||
composeOptions.setMentionedUsernames(mentionedUsernames);
|
||||
|
||||
Intent intent = ComposeActivity.startIntent(getContext(), composeOptions);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
|
@ -205,7 +208,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
|||
|
||||
String loggedInAccountId = null;
|
||||
AccountEntity activeAccount = accountManager.getActiveAccount();
|
||||
if(activeAccount != null) {
|
||||
if (activeAccount != null) {
|
||||
loggedInAccountId = activeAccount.getAccountId();
|
||||
}
|
||||
|
||||
|
@ -238,7 +241,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
|||
|
||||
Menu menu = popup.getMenu();
|
||||
MenuItem openAsItem = menu.findItem(R.id.status_open_as);
|
||||
switch(accounts.size()) {
|
||||
switch (accounts.size()) {
|
||||
case 0:
|
||||
case 1:
|
||||
openAsItem.setVisible(false);
|
||||
|
@ -261,7 +264,8 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
|||
switch (item.getItemId()) {
|
||||
case R.id.status_share_content: {
|
||||
Status statusToShare = status;
|
||||
if(statusToShare.getReblog() != null) statusToShare = statusToShare.getReblog();
|
||||
if (statusToShare.getReblog() != null)
|
||||
statusToShare = statusToShare.getReblog();
|
||||
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.setAction(Intent.ACTION_SEND);
|
||||
|
@ -386,7 +390,8 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
|||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
||||
.subscribe(
|
||||
deletedStatus -> {},
|
||||
deletedStatus -> {
|
||||
},
|
||||
error -> {
|
||||
Log.w("SFragment", "error deleting status", error);
|
||||
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 -> {
|
||||
removeItem(position);
|
||||
|
||||
if(deletedStatus.isEmpty()) {
|
||||
if (deletedStatus.isEmpty()) {
|
||||
deletedStatus = status.toDeletedStatus();
|
||||
}
|
||||
|
||||
ComposeActivity.IntentBuilder intentBuilder = new ComposeActivity.IntentBuilder()
|
||||
.tootText(deletedStatus.getText())
|
||||
.inReplyToId(deletedStatus.getInReplyToId())
|
||||
.visibility(deletedStatus.getVisibility())
|
||||
.contentWarning(deletedStatus.getSpoilerText())
|
||||
.mediaAttachments(deletedStatus.getAttachments())
|
||||
.sensitive(deletedStatus.getSensitive());
|
||||
if(deletedStatus.getPoll() != null) {
|
||||
intentBuilder.poll(deletedStatus.getPoll().toNewPoll(deletedStatus.getCreatedAt()));
|
||||
ComposeOptions composeOptions = new ComposeOptions();
|
||||
composeOptions.setTootText(deletedStatus.getText());
|
||||
composeOptions.setInReplyToId(deletedStatus.getInReplyToId());
|
||||
composeOptions.setVisibility(deletedStatus.getVisibility());
|
||||
composeOptions.setContentWarning(deletedStatus.getSpoilerText());
|
||||
composeOptions.setMediaAttachments(deletedStatus.getAttachments());
|
||||
composeOptions.setSensitive(deletedStatus.getSensitive());
|
||||
if (deletedStatus.getPoll() != null) {
|
||||
composeOptions.setPoll(deletedStatus.getPoll().toNewPoll(deletedStatus.getCreatedAt()));
|
||||
}
|
||||
|
||||
Intent intent = intentBuilder.build(getContext());
|
||||
Intent intent = ComposeActivity
|
||||
.startIntent(getContext(), composeOptions);
|
||||
startActivity(intent);
|
||||
},
|
||||
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.putExtra(MainActivity.STATUS_URL, statusUrl);
|
||||
startActivity(intent);
|
||||
((BaseActivity)getActivity()).finishWithoutSlideOutAnimation();
|
||||
((BaseActivity) getActivity()).finishWithoutSlideOutAnimation();
|
||||
}
|
||||
|
||||
private void showOpenAsDialog(String statusUrl, CharSequence dialogTitle) {
|
||||
BaseActivity activity = (BaseActivity)getActivity();
|
||||
BaseActivity activity = (BaseActivity) getActivity();
|
||||
activity.showAccountChooserDialog(dialogTitle, false, account -> openAsAccount(statusUrl, account));
|
||||
}
|
||||
|
||||
private void downloadAllMedia(Status status) {
|
||||
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();
|
||||
Uri uri = Uri.parse(url);
|
||||
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);
|
||||
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename);
|
||||
downloadManager.enqueue(request);
|
||||
|
@ -467,8 +472,8 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
|||
}
|
||||
|
||||
private void requestDownloadAllMedia(Status status) {
|
||||
String[] permissions = new String[]{ Manifest.permission.WRITE_EXTERNAL_STORAGE };
|
||||
((BaseActivity)getActivity()).requestPermissions(permissions, (permissions1, grantResults) -> {
|
||||
String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
|
||||
((BaseActivity) getActivity()).requestPermissions(permissions, (permissions1, grantResults) -> {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
downloadAllMedia(status);
|
||||
} else {
|
||||
|
@ -516,9 +521,9 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|
|||
@VisibleForTesting
|
||||
public boolean shouldFilterStatus(Status status) {
|
||||
|
||||
if(filterRemoveRegex && status.getPoll() != null) {
|
||||
for(PollOption option: status.getPoll().getOptions()) {
|
||||
if(filterRemoveRegexMatcher.reset(option.getTitle()).find()) {
|
||||
if (filterRemoveRegex && status.getPoll() != null) {
|
||||
for (PollOption option : status.getPoll().getOptions()) {
|
||||
if (filterRemoveRegexMatcher.reset(option.getTitle()).find()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,7 @@ import android.os.Bundle;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.keylesspalace.tusky.ComposeActivity;
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.TimeZone;
|
||||
|
|
|
@ -18,9 +18,10 @@ package com.keylesspalace.tusky.fragment.preference
|
|||
import android.os.Bundle
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.keylesspalace.tusky.ComposeActivity
|
||||
import com.keylesspalace.tusky.PreferencesActivity
|
||||
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.getNonNullString
|
||||
import com.mikepenz.google_material_typeface_library.GoogleMaterial
|
||||
|
@ -120,10 +121,10 @@ class PreferencesFragment : PreferenceFragmentCompat() {
|
|||
val sendCrashReportPreference = requirePreference("sendCrashReport")
|
||||
sendCrashReportPreference.setOnPreferenceClickListener {
|
||||
activity?.let { activity ->
|
||||
val intent = ComposeActivity.IntentBuilder()
|
||||
.tootText("@ars42525@odakyu.app $stackTrace".substring(0, 400))
|
||||
.contentWarning("Yuito StackTrace")
|
||||
.build(activity)
|
||||
val intent = ComposeActivity.startIntent(activity, ComposeOptions(
|
||||
tootText = "@ars42525@odakyu.app $stackTrace".substring(0, 400),
|
||||
contentWarning = "Yuito StackTrace"
|
||||
))
|
||||
activity.startActivity(intent)
|
||||
sharedPreferences.edit()
|
||||
.remove("stack_trace")
|
||||
|
|
|
@ -43,7 +43,7 @@ interface MastodonApi {
|
|||
fun getLists(): Single<List<MastoList>>
|
||||
|
||||
@GET("/api/v1/custom_emojis")
|
||||
fun getCustomEmojis(): Call<List<Emoji>>
|
||||
fun getCustomEmojis(): Single<List<Emoji>>
|
||||
|
||||
@GET("api/v1/instance")
|
||||
fun getInstance(): Single<Instance>
|
||||
|
@ -116,14 +116,14 @@ interface MastodonApi {
|
|||
@POST("api/v1/media")
|
||||
fun uploadMedia(
|
||||
@Part file: MultipartBody.Part
|
||||
): Call<Attachment>
|
||||
): Single<Attachment>
|
||||
|
||||
@FormUrlEncoded
|
||||
@PUT("api/v1/media/{mediaId}")
|
||||
fun updateMedia(
|
||||
@Path("mediaId") mediaId: String,
|
||||
@Field("description") description: String
|
||||
): Call<Attachment>
|
||||
): Single<Attachment>
|
||||
|
||||
@POST("api/v1/statuses")
|
||||
fun createStatus(
|
||||
|
@ -238,10 +238,10 @@ interface MastodonApi {
|
|||
|
||||
@GET("api/v1/accounts/search")
|
||||
fun searchAccounts(
|
||||
@Query("q") q: String,
|
||||
@Query("resolve") resolve: Boolean?,
|
||||
@Query("limit") limit: Int?,
|
||||
@Query("following") following: Boolean?
|
||||
@Query("q") query: String,
|
||||
@Query("resolve") resolve: Boolean? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
@Query("following") following: Boolean? = null
|
||||
): Single<List<Account>>
|
||||
|
||||
@GET("api/v1/accounts/{id}")
|
||||
|
@ -318,6 +318,11 @@ interface MastodonApi {
|
|||
@Query("id[]") accountIds: List<String>
|
||||
): Call<List<Relationship>>
|
||||
|
||||
@GET("api/v1/accounts/{id}/identity_proofs")
|
||||
fun identityProofs(
|
||||
@Path("id") accountId: String
|
||||
): Call<List<IdentityProof>>
|
||||
|
||||
@GET("api/v1/blocks")
|
||||
fun blocks(
|
||||
@Query("max_id") maxId: String?
|
||||
|
|
|
@ -23,12 +23,15 @@ import androidx.core.app.NotificationCompat
|
|||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.RemoteInput
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.keylesspalace.tusky.ComposeActivity
|
||||
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.entity.Status
|
||||
import com.keylesspalace.tusky.service.SendTootService
|
||||
import com.keylesspalace.tusky.service.TootToSend
|
||||
import com.keylesspalace.tusky.util.NotificationHelper
|
||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||
import dagger.android.AndroidInjection
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -85,19 +88,25 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
|||
|
||||
val sendIntent = SendTootService.sendTootIntent(
|
||||
context,
|
||||
text,
|
||||
spoiler,
|
||||
visibility,
|
||||
false,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
null,
|
||||
citedStatusId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null, null, account, 0)
|
||||
TootToSend(
|
||||
text,
|
||||
spoiler,
|
||||
visibility.serverString(),
|
||||
false,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
null,
|
||||
citedStatusId,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null, null, account.id,
|
||||
0,
|
||||
randomAlphanumericString(16),
|
||||
0
|
||||
)
|
||||
)
|
||||
|
||||
context.startService(sendIntent)
|
||||
|
||||
|
@ -125,14 +134,14 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
|||
|
||||
accountManager.setActiveAccount(senderId)
|
||||
|
||||
val composeIntent = ComposeActivity.IntentBuilder()
|
||||
.inReplyToId(citedStatusId)
|
||||
.replyVisibility(visibility)
|
||||
.contentWarning(spoiler)
|
||||
.mentionedUsernames(mentions.toList())
|
||||
.replyingStatusAuthor(localAuthorId)
|
||||
.replyingStatusContent(citedText)
|
||||
.build(context)
|
||||
val composeIntent = ComposeActivity.startIntent(context, ComposeOptions(
|
||||
inReplyToId = citedStatusId,
|
||||
replyVisibility = visibility,
|
||||
contentWarning = spoiler,
|
||||
mentionedUsernames = mentions.toSet(),
|
||||
replyingStatusAuthor = localAuthorId,
|
||||
replyingStatusContent = citedText
|
||||
))
|
||||
|
||||
composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@ import android.content.ClipData
|
|||
import android.content.ClipDescription
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
|
@ -19,7 +18,6 @@ import com.keylesspalace.tusky.R
|
|||
import com.keylesspalace.tusky.appstore.EventHub
|
||||
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
||||
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
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.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.SaveTootHelper
|
||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||
import dagger.android.AndroidInjection
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import retrofit2.Call
|
||||
|
@ -50,7 +47,8 @@ class SendTootService : Service(), Injectable {
|
|||
@Inject
|
||||
lateinit var database: AppDatabase
|
||||
|
||||
private lateinit var saveTootHelper: SaveTootHelper
|
||||
@Inject
|
||||
lateinit var saveTootHelper: SaveTootHelper
|
||||
|
||||
private val tootsToSend = ConcurrentHashMap<Int, TootToSend>()
|
||||
private val sendCalls = ConcurrentHashMap<Int, Call<Status>>()
|
||||
|
@ -61,7 +59,6 @@ class SendTootService : Service(), Injectable {
|
|||
|
||||
override fun onCreate() {
|
||||
AndroidInjection.inject(this)
|
||||
saveTootHelper = SaveTootHelper(database.tootDao(), this)
|
||||
super.onCreate()
|
||||
}
|
||||
|
||||
|
@ -285,56 +282,19 @@ class SendTootService : Service(), Injectable {
|
|||
|
||||
@JvmStatic
|
||||
fun sendTootIntent(context: Context,
|
||||
text: String,
|
||||
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
|
||||
tootToSend: TootToSend
|
||||
): Intent {
|
||||
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)
|
||||
|
||||
if(mediaUris.isNotEmpty()) {
|
||||
if (tootToSend.mediaUris.isNotEmpty()) {
|
||||
// forward uri permissions
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
val uriClip = ClipData(
|
||||
ClipDescription("Toot Media", arrayOf("image/*", "video/*")),
|
||||
ClipData.Item(mediaUris[0])
|
||||
ClipData.Item(tootToSend.mediaUris[0])
|
||||
)
|
||||
mediaUris
|
||||
tootToSend.mediaUris
|
||||
.drop(1)
|
||||
.forEach { mediaUri ->
|
||||
uriClip.addItem(ClipData.Item(mediaUri))
|
||||
|
@ -351,21 +311,23 @@ class SendTootService : Service(), Injectable {
|
|||
}
|
||||
|
||||
@Parcelize
|
||||
data class TootToSend(val text: String,
|
||||
val warningText: String,
|
||||
val visibility: String,
|
||||
val sensitive: Boolean,
|
||||
val mediaIds: List<String>,
|
||||
val mediaUris: List<String>,
|
||||
val mediaDescriptions: List<String>,
|
||||
val scheduledAt: String?,
|
||||
val inReplyToId: String?,
|
||||
val poll: NewPoll?,
|
||||
val replyingStatusContent: String?,
|
||||
val replyingStatusAuthorUsername: String?,
|
||||
val savedJsonUrls: String?,
|
||||
val quoteId: String?,
|
||||
val accountId: Long,
|
||||
val savedTootUid: Int,
|
||||
val idempotencyKey: String,
|
||||
var retries: Int) : Parcelable
|
||||
data class TootToSend(
|
||||
val text: String,
|
||||
val warningText: String,
|
||||
val visibility: String,
|
||||
val sensitive: Boolean,
|
||||
val mediaIds: List<String>,
|
||||
val mediaUris: List<String>,
|
||||
val mediaDescriptions: List<String>,
|
||||
val scheduledAt: String?,
|
||||
val inReplyToId: String?,
|
||||
val poll: NewPoll?,
|
||||
val replyingStatusContent: String?,
|
||||
val replyingStatusAuthorUsername: String?,
|
||||
val savedJsonUrls: List<String>?,
|
||||
val quoteId: String?,
|
||||
val accountId: Long,
|
||||
val savedTootUid: Int,
|
||||
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.content.Intent
|
||||
import android.service.quicksettings.TileService
|
||||
|
||||
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.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
|
@ -68,8 +69,8 @@ public class LinkHelper {
|
|||
* @param listener to notify about particular spans that are clicked
|
||||
*/
|
||||
public static void setClickableText(TextView view, Spanned content,
|
||||
@Nullable Status.Mention[] mentions, final LinkListener listener,
|
||||
boolean removeQuote) {
|
||||
@Nullable Status.Mention[] mentions, final LinkListener listener,
|
||||
boolean removeQuote) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(content);
|
||||
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class);
|
||||
for (URLSpan span : urlSpans) {
|
||||
|
@ -186,6 +187,14 @@ public class LinkHelper {
|
|||
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
|
||||
*
|
||||
|
@ -229,10 +238,17 @@ public class LinkHelper {
|
|||
public static void openLinkInCustomTab(Uri uri, Context context) {
|
||||
int toolbarColor = ThemeUtils.getColor(context, R.attr.custom_tab_toolbar);
|
||||
|
||||
CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder()
|
||||
CustomTabsIntent.Builder customTabsIntentBuilder = new CustomTabsIntent.Builder()
|
||||
.setToolbarColor(toolbarColor)
|
||||
.setShowTitle(true)
|
||||
.build();
|
||||
.setShowTitle(true);
|
||||
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||
customTabsIntentBuilder.setNavigationBarColor(
|
||||
ThemeUtils.getColor(context, android.R.attr.navigationBarColor)
|
||||
);
|
||||
}
|
||||
|
||||
CustomTabsIntent customTabsIntent = customTabsIntentBuilder.build();
|
||||
try {
|
||||
customTabsIntent.launchUrl(context, uri);
|
||||
} 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
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Matrix
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.media.ThumbnailUtils
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
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)
|
||||
fun getImageSquarePixels(contentResolver: ContentResolver, uri: Uri): Long {
|
||||
val input = contentResolver.openInputStream(uri)
|
||||
|
|
|
@ -5,16 +5,18 @@ import android.content.ContentResolver;
|
|||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.FileProvider;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
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.reflect.TypeToken;
|
||||
import com.keylesspalace.tusky.BuildConfig;
|
||||
import com.keylesspalace.tusky.db.AppDatabase;
|
||||
import com.keylesspalace.tusky.db.TootDao;
|
||||
import com.keylesspalace.tusky.db.TootEntity;
|
||||
import com.keylesspalace.tusky.entity.NewPoll;
|
||||
|
@ -27,6 +29,8 @@ import java.util.Date;
|
|||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
public final class SaveTootHelper {
|
||||
|
||||
private static final String TAG = "SaveTootHelper";
|
||||
|
@ -35,15 +39,16 @@ public final class SaveTootHelper {
|
|||
private Context context;
|
||||
private Gson gson = new Gson();
|
||||
|
||||
public SaveTootHelper(@NonNull TootDao tootDao, @NonNull Context context) {
|
||||
this.tootDao = tootDao;
|
||||
@Inject
|
||||
public SaveTootHelper(@NonNull AppDatabase appDatabase, @NonNull Context context) {
|
||||
this.tootDao = appDatabase.tootDao();
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public boolean saveToot(@NonNull String content,
|
||||
@NonNull String contentWarning,
|
||||
@Nullable String savedJsonUrls,
|
||||
@Nullable List<String> savedJsonUrls,
|
||||
@NonNull List<String> mediaUris,
|
||||
@NonNull List<String> mediaDescriptions,
|
||||
int savedTootUid,
|
||||
|
@ -58,31 +63,25 @@ public final class SaveTootHelper {
|
|||
}
|
||||
|
||||
// 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 mediaDescriptionsSerialized = null;
|
||||
|
||||
if (!ListUtils.isEmpty(mediaUris)) {
|
||||
List<String> savedList = saveMedia(mediaUris, existingUris);
|
||||
List<String> savedList = saveMedia(mediaUris, savedJsonUrls);
|
||||
if (!ListUtils.isEmpty(savedList)) {
|
||||
mediaUrlsSerialized = gson.toJson(savedList);
|
||||
if (!ListUtils.isEmpty(existingUris)) {
|
||||
deleteMedia(setDifference(existingUris, savedList));
|
||||
if (!ListUtils.isEmpty(savedJsonUrls)) {
|
||||
deleteMedia(setDifference(savedJsonUrls, savedList));
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
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
|
||||
* can be deleted. */
|
||||
deleteMedia(existingUris);
|
||||
deleteMedia(savedJsonUrls);
|
||||
}
|
||||
final TootEntity toot = new TootEntity(savedTootUid, content, mediaUrlsSerialized, mediaDescriptionsSerialized, contentWarning,
|
||||
inReplyToId,
|
||||
|
@ -103,15 +102,16 @@ public final class SaveTootHelper {
|
|||
|
||||
public void deleteDraft(int tootId) {
|
||||
TootEntity item = tootDao.find(tootId);
|
||||
if(item != null) {
|
||||
if (item != null) {
|
||||
deleteDraft(item);
|
||||
}
|
||||
}
|
||||
|
||||
public void deleteDraft(@NonNull TootEntity item){
|
||||
public void deleteDraft(@NonNull TootEntity item) {
|
||||
// Delete any media files associated with the status.
|
||||
ArrayList<String> uris = gson.fromJson(item.getUrls(),
|
||||
new TypeToken<ArrayList<String>>() {}.getType());
|
||||
new TypeToken<ArrayList<String>>() {
|
||||
}.getType());
|
||||
if (uris != null) {
|
||||
for (String uriString : uris) {
|
||||
Uri uri = Uri.parse(uriString);
|
||||
|
@ -172,7 +172,7 @@ public final class SaveTootHelper {
|
|||
}
|
||||
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());
|
||||
}
|
||||
return results;
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
|
||||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
@ -24,7 +26,7 @@ public class VersionUtils {
|
|||
private int minor;
|
||||
private int patch;
|
||||
|
||||
public VersionUtils(String versionString) {
|
||||
public VersionUtils(@NonNull String versionString) {
|
||||
String regex = "([0-9]+)\\.([0-9]+)\\.([0-9]+).*";
|
||||
Pattern pattern = Pattern.compile(regex);
|
||||
Matcher matcher = pattern.matcher(versionString);
|
||||
|
|
|
@ -51,4 +51,13 @@ inline fun EditText.onTextChanged(
|
|||
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.db.AccountManager
|
||||
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.network.MastodonApi
|
||||
import com.keylesspalace.tusky.util.Error
|
||||
import com.keylesspalace.tusky.util.Loading
|
||||
import com.keylesspalace.tusky.util.Resource
|
||||
import com.keylesspalace.tusky.util.Success
|
||||
import com.keylesspalace.tusky.util.*
|
||||
import io.reactivex.disposables.Disposable
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
|
@ -27,6 +26,14 @@ class AccountViewModel @Inject constructor(
|
|||
val accountData = MutableLiveData<Resource<Account>>()
|
||||
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 disposable: Disposable = eventHub.events
|
||||
.subscribe { event ->
|
||||
|
@ -60,6 +67,7 @@ class AccountViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onFailure(call: Call<Account>, t: Throwable) {
|
||||
Log.w(TAG, "failed obtaining account", t)
|
||||
accountData.postValue(Error())
|
||||
isDataLoading = false
|
||||
isRefreshing.postValue(false)
|
||||
|
@ -90,6 +98,7 @@ class AccountViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
override fun onFailure(call: Call<List<Relationship>>, t: Throwable) {
|
||||
Log.w(TAG, "failed obtaining relationships", t)
|
||||
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() {
|
||||
val relationship = relationshipData.value?.data
|
||||
if (relationship?.following == true || relationship?.requested == true) {
|
||||
|
@ -227,6 +260,7 @@ class AccountViewModel @Inject constructor(
|
|||
return
|
||||
accountId.let {
|
||||
obtainAccount(isReload)
|
||||
obtainIdentityProof()
|
||||
if (!isSelf)
|
||||
obtainRelationship(isReload)
|
||||
}
|
||||
|
|
|
@ -12,13 +12,13 @@ import android.widget.TextView;
|
|||
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
|
||||
import com.keylesspalace.tusky.ComposeActivity;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.appstore.DrawerFooterClickedEvent;
|
||||
import com.keylesspalace.tusky.appstore.Event;
|
||||
import com.keylesspalace.tusky.appstore.EventHub;
|
||||
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent;
|
||||
import com.keylesspalace.tusky.appstore.QuickReplyEvent;
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity;
|
||||
import com.keylesspalace.tusky.db.AccountEntity;
|
||||
import com.keylesspalace.tusky.db.AccountManager;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
|
@ -28,8 +28,9 @@ import java.util.Arrays;
|
|||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import static com.keylesspalace.tusky.ComposeActivity.PREF_DEFAULT_TAG;
|
||||
import static com.keylesspalace.tusky.ComposeActivity.PREF_USE_DEFAULT_TAG;
|
||||
import static com.keylesspalace.tusky.components.compose.ComposeActivity.CAN_USE_UNLEAKABLE;
|
||||
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 {
|
||||
|
||||
|
@ -74,8 +75,7 @@ public class QuickTootHelper {
|
|||
|
||||
public void composeButton() {
|
||||
if (tootEditText.getText().length() == 0 && inReplyTo == null) {
|
||||
Intent composeIntent = new Intent(context, ComposeActivity.class);
|
||||
context.startActivity(composeIntent);
|
||||
context.startActivity(getComposeIntent(context, true, false));
|
||||
} else {
|
||||
startComposeWithQuickComposeData();
|
||||
}
|
||||
|
@ -107,43 +107,45 @@ public class QuickTootHelper {
|
|||
}
|
||||
|
||||
private void startComposeWithQuickComposeData() {
|
||||
Intent composeIntent = setupIntentBuilder(false);
|
||||
Intent intent = getComposeIntent(context, false, false);
|
||||
resetQuickCompose();
|
||||
context.startActivity(composeIntent);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
private void quickToot() {
|
||||
if (tootEditText.getText().toString().length() > 0) {
|
||||
Intent composeIntent = setupIntentBuilder(true);
|
||||
Intent intent = getComposeIntent(context, false, true);
|
||||
resetQuickCompose();
|
||||
context.startActivity(composeIntent);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
private Intent setupIntentBuilder(boolean tootRightNow) {
|
||||
ComposeActivity.IntentBuilder intentBuilder = new ComposeActivity.IntentBuilder()
|
||||
.tootText(tootEditText.getText().toString())
|
||||
.visibility(getCurrentVisibility())
|
||||
.tootRightNow(tootRightNow);
|
||||
private Intent getComposeIntent(Context context, boolean onlyVisibility, boolean tootRightNow) {
|
||||
ComposeActivity.ComposeOptions options = new ComposeActivity.ComposeOptions();
|
||||
options.setVisibility(getCurrentVisibility());
|
||||
if (onlyVisibility) {
|
||||
return ComposeActivity.startIntent(context, options);
|
||||
}
|
||||
options.setTootText(tootEditText.getText().toString());
|
||||
options.setTootRightNow(tootRightNow);
|
||||
|
||||
if (inReplyTo == null) {
|
||||
return intentBuilder.build(context);
|
||||
if (inReplyTo != null) {
|
||||
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();
|
||||
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);
|
||||
return ComposeActivity.startIntent(context, options);
|
||||
}
|
||||
|
||||
private void resetQuickCompose() {
|
||||
|
@ -178,7 +180,7 @@ public class QuickTootHelper {
|
|||
|
||||
private Status.Visibility getCurrentVisibility() {
|
||||
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) {
|
||||
defPrefs.edit()
|
||||
.putInt(PREF_CURRENT_VISIBILITY, Status.Visibility.PUBLIC.getNum())
|
||||
|
@ -217,8 +219,7 @@ public class QuickTootHelper {
|
|||
visibility = Status.Visibility.PRIVATE;
|
||||
break;
|
||||
case PRIVATE:
|
||||
if (Arrays.asList(ComposeActivity.CAN_USE_UNLEAKABLE)
|
||||
.contains(domain)) {
|
||||
if (Arrays.asList(CAN_USE_UNLEAKABLE).contains(domain)) {
|
||||
visibility = Status.Visibility.UNLEAKABLE;
|
||||
} else {
|
||||
visibility = Status.Visibility.PUBLIC;
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/activity_compose"
|
||||
android:id="@+id/activityCompose"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
|
@ -30,10 +30,9 @@
|
|||
android:layout_gravity="end"
|
||||
android:padding="8dp"
|
||||
android:text="@string/at_symbol"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
android:textSize="?attr/status_text_large"
|
||||
/>
|
||||
android:textStyle="bold" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatButton
|
||||
android:id="@+id/hashButton"
|
||||
|
@ -43,10 +42,9 @@
|
|||
android:layout_gravity="end"
|
||||
android:padding="8dp"
|
||||
android:text="@string/hash_symbol"
|
||||
android:textStyle="bold"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
android:textSize="?attr/status_text_large"
|
||||
/>
|
||||
android:textStyle="bold" />
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
|
@ -131,7 +129,7 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<com.keylesspalace.tusky.view.EditTextTyped
|
||||
<com.keylesspalace.tusky.components.compose.view.EditTextTyped
|
||||
android:id="@+id/composeEditField"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -148,25 +146,19 @@
|
|||
android:textColorHint="?android:attr/textColorTertiary"
|
||||
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_height="wrap_content"
|
||||
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>
|
||||
android:scrollbars="none" />
|
||||
|
||||
<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>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
|
@ -178,7 +170,7 @@
|
|||
android:paddingBottom="52dp" >
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/checkbox_use_default_text"
|
||||
android:id="@+id/checkboxUseDefaultText"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
|
@ -186,14 +178,14 @@
|
|||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<EditText
|
||||
android:id="@+id/edittext_default_text"
|
||||
android:id="@+id/editTextDefaultText"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/hint_default_text"
|
||||
android:inputType="text"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/checkbox_use_default_text"
|
||||
app:layout_constraintStart_toEndOf="@id/checkboxUseDefaultText"
|
||||
tools:ignore="Autofill" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -214,7 +206,7 @@
|
|||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/action_photo_take"
|
||||
android:id="@+id/actionPhotoTake"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="8dp"
|
||||
|
@ -223,7 +215,7 @@
|
|||
android:textSize="?attr/status_text_medium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/action_photo_pick"
|
||||
android:id="@+id/actionPhotoPick"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="8dp"
|
||||
|
@ -232,7 +224,7 @@
|
|||
android:textSize="?attr/status_text_medium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/action_add_poll"
|
||||
android:id="@+id/addPollTextActionTextView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawablePadding="8dp"
|
||||
|
@ -257,7 +249,7 @@
|
|||
app:behavior_peekHeight="0dp"
|
||||
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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -271,7 +263,7 @@
|
|||
app:behavior_peekHeight="0dp"
|
||||
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:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -340,7 +332,7 @@
|
|||
android:contentDescription="@string/action_content_warning"
|
||||
android:padding="4dp"
|
||||
android:tooltipText="@string/action_content_warning"
|
||||
app:srcCompat="@drawable/ic_cw_24dp"/>
|
||||
app:srcCompat="@drawable/ic_cw_24dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/composeEmojiButton"
|
||||
|
@ -377,7 +369,7 @@
|
|||
android:textSize="?attr/status_text_medium"
|
||||
tools:text="500" />
|
||||
|
||||
<com.keylesspalace.tusky.view.TootButton
|
||||
<com.keylesspalace.tusky.components.compose.view.TootButton
|
||||
android:id="@+id/composeTootButton"
|
||||
style="@style/TuskyButton"
|
||||
android:layout_width="@dimen/toot_button_width"
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
android:background="?android:colorBackground"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="16dp">
|
||||
|
||||
<ImageView
|
||||
|
@ -15,6 +15,8 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:src="@drawable/ic_drag_indicator_24dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
@ -24,6 +26,8 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:layout_weight="1"
|
||||
android:drawablePadding="12dp"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
|
@ -32,10 +36,23 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_goneMarginBottom="16dp"
|
||||
app:layout_goneMarginBottom="8dp"
|
||||
tools:drawableStart="@drawable/ic_home_24dp"
|
||||
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
|
||||
android:id="@+id/chipGroup"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -56,4 +73,3 @@
|
|||
</com.google.android.material.chip.ChipGroup>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
|
@ -5,27 +5,28 @@
|
|||
|
||||
<Button
|
||||
android:id="@+id/resetScheduleButton"
|
||||
style="@style/TuskyButton.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
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
|
||||
android:id="@+id/scheduledDateTime"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:drawablePadding="4dp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
android:textSize="?attr/status_text_medium"
|
||||
android:drawablePadding="4dp"
|
||||
app:layout_constraintBottom_toBottomOf="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" />
|
||||
|
||||
</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_schedule_toot">برمجة تبويق</string>
|
||||
<string name="action_reset_schedule">صفّر</string>
|
||||
<string name="hint_configure_scheduled_toot">اضغط هنا لضبط برمجة التبويق.</string>
|
||||
<string name="post_lookup_error_format">خطأ أثناء البحث عن منشور %s</string>
|
||||
|
||||
<string name="title_bookmarks">الفواصل المرجعية</string>
|
||||
|
@ -495,4 +494,6 @@
|
|||
<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>
|
||||
|
|
|
@ -508,7 +508,6 @@
|
|||
<string name="action_access_scheduled_toot">নির্ধারিত টুটগুলি</string>
|
||||
<string name="action_schedule_toot">নির্ধারিত টুট</string>
|
||||
<string name="action_reset_schedule">রিসেট</string>
|
||||
<string name="hint_configure_scheduled_toot">নির্ধারিত টুট কনফিগার করতে এখানে আলতো চাপুন।</string>
|
||||
<string name="about_powered_by_tusky">টাস্কি দ্বারা চালিত</string>
|
||||
<string name="post_lookup_error_format">%s পোস্ট অনুসন্ধানে ত্রুটি</string>
|
||||
|
||||
|
|
|
@ -518,4 +518,18 @@
|
|||
<string name="add_poll_choice">Afegeix una tria</string>
|
||||
<string name="poll_allow_multiple_choices">Múltiples tries</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_schedule_toot">Naplánovat toot</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="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>
|
||||
|
|
|
@ -451,6 +451,5 @@
|
|||
<string name="action_access_scheduled_toot">Geplante Beiträge</string>
|
||||
<string name="action_schedule_toot">Plane Beitrag</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>
|
||||
</resources>
|
||||
|
|
|
@ -456,4 +456,18 @@
|
|||
<string name="poll_new_choice_hint">Elekton %d</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>
|
||||
|
|
|
@ -465,7 +465,6 @@
|
|||
<string name="action_access_scheduled_toot">Estados programados</string>
|
||||
<string name="action_schedule_toot">Programar estado</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="about_powered_by_tusky">Potenciado por Tusky</string>
|
||||
|
@ -473,4 +472,6 @@
|
|||
<string name="action_bookmark">Favorito</string>
|
||||
<string name="action_view_bookmarks">Favoritos</string>
|
||||
<string name="description_status_bookmarked">Marcado como favorito</string>
|
||||
<string name="select_list_title">Seleccionar lista</string>
|
||||
<string name="list">Lista</string>
|
||||
</resources>
|
||||
|
|
|
@ -311,7 +311,6 @@
|
|||
|
||||
<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="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>
|
||||
|
|
|
@ -460,7 +460,6 @@
|
|||
<string name="action_access_scheduled_toot">بوقهای زمانبندیشده</string>
|
||||
<string name="action_schedule_toot">زمانبندی بوق</string>
|
||||
<string name="action_reset_schedule">بازنشانی</string>
|
||||
<string name="hint_configure_scheduled_toot">برای پیکربندی بوق زمانبندیشده، اینجا را بزنید.</string>
|
||||
<string name="mute_domain_warning">مطمئنید میخواهید تمام %s را مسدود کنید؟ محتوای آن دامنه را در هیچیک از خط زمانیها یا در آگاهیهایتان نخواهید دید. پیروانتان از آن دامنه، برداشته خواهند شد.</string>
|
||||
<string name="filter_dialog_whole_word_description">هنگامی که کلیدواژه یا عبارت، فقط حروفعددی باشد، فقط اگر با تمام واژه مطابق باشد، اعمال خواهد شد</string>
|
||||
<string name="filter_add_description">عبارت پالایش</string>
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
<string name="title_direct_messages">Messages directs</string>
|
||||
<string name="title_tab_preferences">Onglets</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_pinned">Épinglés</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_filter_mentions">on me mentionne</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_favourites">mes pouets sont mis en favoris</string>
|
||||
<string name="pref_title_notification_filter_reblogs">mes messages sont boostés</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_app_theme">Thème de l’application</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_schedule_toot">Planifier le pouet</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 recherche du post %s</string>
|
||||
<string name="post_lookup_error_format">Erreur lors de la récupération du message %s</string>
|
||||
|
||||
<string name="about_powered_by_tusky">Propulsé par Tusky</string>
|
||||
<string name="title_bookmarks">Signets</string>
|
||||
<string name="action_bookmark">Marquer comme signet</string>
|
||||
<string name="action_view_bookmarks">Signets</string>
|
||||
<string name="description_status_bookmarked">Marqué comme un signet</string>
|
||||
<string name="title_bookmarks">Marque-pages</string>
|
||||
<string name="action_bookmark">Ajouter aux marque-pages</string>
|
||||
<string name="action_view_bookmarks">Marque-pages</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>
|
||||
|
|
|
@ -470,7 +470,13 @@
|
|||
<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_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>
|
||||
|
||||
</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="pin_action">Fissa</string>
|
||||
<plurals name="favs">
|
||||
<item quantity="one"><b>%1$s</b> Mi piace</item>
|
||||
<item quantity="other"><b>%1$s</b> Mi piace</item>
|
||||
</plurals>
|
||||
<item quantity="one"><b>%1$s</b> Mi piace</item>
|
||||
<item quantity="other"><b>%1$s</b> Mi piace</item>
|
||||
</plurals>
|
||||
<plurals name="reblogs">
|
||||
<item quantity="one"><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="action_view_domain_mutes">Domini nascosti</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="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_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="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>
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
<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_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_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_schedule_toot">Planlegg toot</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="about_powered_by_tusky">Drevet av Tusky</string>
|
||||
|
@ -515,4 +514,6 @@
|
|||
<string name="action_bookmark">Bokmerke</string>
|
||||
<string name="action_view_bookmarks">Bokmerker</string>
|
||||
<string name="description_status_bookmarked">Bokmerke lagt til</string>
|
||||
<string name="select_list_title">Velg liste</string>
|
||||
<string name="list">Liste</string>
|
||||
</resources>
|
||||
|
|
|
@ -477,7 +477,6 @@
|
|||
<string name="action_access_scheduled_toot">Tuts planificats</string>
|
||||
<string name="action_schedule_toot">Planificar de tuts</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="about_powered_by_tusky">Propulsat per Tusky</string>
|
||||
|
@ -485,4 +484,6 @@
|
|||
<string name="action_bookmark">Ajustar als marcapaginas</string>
|
||||
<string name="action_view_bookmarks">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>
|
||||
|
|
|
@ -368,9 +368,9 @@
|
|||
<string name="pin_action">Przypnij do profilu</string>
|
||||
|
||||
<plurals name="favs">
|
||||
<item quantity="one"><b>%1$s</b> polubienie</item>
|
||||
<item quantity="few"><b>%1$s</b> polubienia</item>
|
||||
<item quantity="many"><b>%1$s</b> polubień</item>
|
||||
<item quantity="one"><b>%1$s</b> polubienie</item>
|
||||
<item quantity="few"><b>%1$s</b> polubienia</item>
|
||||
<item quantity="many"><b>%1$s</b> polubień</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="reblogs">
|
||||
|
@ -484,7 +484,6 @@
|
|||
<string name="action_access_scheduled_toot">Zaplanowane wpisy</string>
|
||||
<string name="action_schedule_toot">Zaplanuj wpis</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="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_view_bookmarks">Zakładki</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>
|
||||
|
|
|
@ -191,9 +191,9 @@
|
|||
<string name="status_text_size_large">Grande</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_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_boost_name">Boosts</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="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_sa_4">CC-BY-SA 4.0</string>
|
||||
|
||||
|
@ -467,17 +467,18 @@
|
|||
<string name="poll_new_choice_hint">Opção %d</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_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_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="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="about_powered_by_tusky">Desenvolvido por Tusky</string>
|
||||
<string name="description_status_bookmarked">Salvo</string>
|
||||
<string name="select_list_title">Selecionar lista</string>
|
||||
<string name="list">Lista</string>
|
||||
</resources>
|
||||
|
|
|
@ -540,7 +540,13 @@
|
|||
<string name="action_access_scheduled_toot">Отложенные записи</string>
|
||||
<string name="action_schedule_toot">Отложить запись</string>
|
||||
<string name="action_reset_schedule">Сброс</string>
|
||||
<string name="hint_configure_scheduled_toot">Нажмите для выбора времени отправки.</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_reset_schedule">Ponastavi</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="about_powered_by_tusky">Poganja ga Tusky</string>
|
||||
|
|
|
@ -468,7 +468,6 @@
|
|||
<string name="action_access_scheduled_toot">Schemalagda toots</string>
|
||||
<string name="action_schedule_toot">Schemalägg toot</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>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
<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="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="report_username_format">\@%s bildir</string>
|
||||
<string name="report_comment_hint">Daha fazla yorum?</string>
|
||||
|
@ -294,8 +294,8 @@
|
|||
<item quantity="other"><b>%1$s</b> Favoriler</item>
|
||||
</plurals>
|
||||
<plurals name="reblogs">
|
||||
<item quantity="one"><b>%s</b> Yinelenen</item>
|
||||
<item quantity="other"><b>%s</b> Yinelenenler</item>
|
||||
<item quantity="one"><b>%s</b> Yinelenen</item>
|
||||
<item quantity="other"><b>%s</b> Yinelenenler</item>
|
||||
</plurals>
|
||||
<string name="title_reblogged_by">tarafından yinelendi</string>
|
||||
<string name="title_favourited_by">Tarafından favorilendi</string>
|
||||
|
@ -425,4 +425,36 @@
|
|||
<string name="title_mentions_dialog">Bahsedenler</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>
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
<attr name="account_toolbar_icon_tint_uncollapsed" 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_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_reply_content_background" 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="tab_page_margin_dark">#1a1c23</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="compose_reply_content_background_dark">#373c4b</color>
|
||||
<color name="autocomplete_divider_dark">#424a5b</color>
|
||||
|
@ -54,7 +54,7 @@
|
|||
<color name="status_divider_light">#cfcfcf</color>
|
||||
<color name="tab_page_margin_light">#cfcfcf</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="custom_tab_toolbar_light">#ffffff</color>
|
||||
<color name="compose_reply_content_background_light">#e0e1e6</color>
|
||||
|
|
|
@ -168,7 +168,6 @@
|
|||
|
||||
<string name="hint_domain">Which instance?</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_display_name">Display name</string>
|
||||
<string name="hint_note">Bio</string>
|
||||
|
|
|
@ -108,7 +108,7 @@
|
|||
</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 name="compose_content_warning_bar_background">@drawable/border_background_light</item>
|
||||
<item name="compose_reply_content_background">
|
||||
|
|
|
@ -18,16 +18,21 @@ package com.keylesspalace.tusky
|
|||
|
||||
import android.text.SpannedString
|
||||
import android.widget.EditText
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
import com.keylesspalace.tusky.db.AccountManager
|
||||
import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.db.InstanceDao
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||
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.Emoji
|
||||
import com.keylesspalace.tusky.entity.Instance
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import okhttp3.Request
|
||||
import org.junit.Assert
|
||||
import com.keylesspalace.tusky.service.ServiceClient
|
||||
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.Before
|
||||
import org.junit.Test
|
||||
|
@ -35,15 +40,8 @@ import org.junit.runner.RunWith
|
|||
import org.mockito.Mockito.`when`
|
||||
import org.mockito.Mockito.mock
|
||||
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.fakes.RoboMenuItem
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
|
||||
/**
|
||||
* Created by charlag on 3/7/18.
|
||||
|
@ -52,14 +50,15 @@ import retrofit2.Response
|
|||
@Config(application = FakeTuskyApplication::class, sdk = [28])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ComposeActivityTest {
|
||||
|
||||
private lateinit var activity: ComposeActivity
|
||||
private lateinit var accountManagerMock: AccountManager
|
||||
private lateinit var apiMock: MastodonApi
|
||||
|
||||
private val instanceDomain = "example.domain"
|
||||
|
||||
private val account = AccountEntity(
|
||||
id = 1,
|
||||
domain = "example.token",
|
||||
domain = instanceDomain,
|
||||
accessToken = "token",
|
||||
isActive = true,
|
||||
accountId = "1",
|
||||
|
@ -83,30 +82,10 @@ class ComposeActivityTest {
|
|||
activity = controller.get()
|
||||
|
||||
accountManagerMock = mock(AccountManager::class.java)
|
||||
`when`(accountManagerMock.activeAccount).thenReturn(account)
|
||||
|
||||
apiMock = mock(MastodonApi::class.java)
|
||||
`when`(apiMock.getCustomEmojis()).thenReturn(object: Call<List<Emoji>> {
|
||||
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.getCustomEmojis()).thenReturn(Single.just(emptyList()))
|
||||
`when`(apiMock.getInstance()).thenReturn(object: Single<Instance>() {
|
||||
override fun subscribeActual(observer: SingleObserver<in Instance>) {
|
||||
val instance = instanceResponseCallback?.invoke()
|
||||
|
@ -119,15 +98,27 @@ class ComposeActivityTest {
|
|||
})
|
||||
|
||||
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)
|
||||
`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.database = dbMock
|
||||
|
||||
`when`(accountManagerMock.activeAccount).thenReturn(account)
|
||||
|
||||
activity.viewModelFactory = viewModelFactoryMock
|
||||
|
||||
controller.create().start()
|
||||
}
|
||||
|
@ -164,7 +155,7 @@ class ComposeActivityTest {
|
|||
fun whenMaximumTootCharsIsNull_defaultLimitIsUsed() {
|
||||
instanceResponseCallback = { getInstanceWithMaximumTootCharacters(null) }
|
||||
setupActivity()
|
||||
assertEquals(ComposeActivity.STATUS_CHARACTER_LIMIT, activity.maximumTootCharacters)
|
||||
assertEquals(DEFAULT_CHARACTER_LIMIT, activity.maximumTootCharacters)
|
||||
}
|
||||
|
||||
@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 additionalContent = " Check out this @image #search result: "
|
||||
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
|
||||
|
@ -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 additionalContent = " Check out this @image #search result: "
|
||||
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() {
|
||||
|
@ -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.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.3.50'
|
||||
ext.kotlin_version = '1.3.61'
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta07'
|
||||
classpath 'com.android.tools.build:gradle:3.5.2'
|
||||
classpath 'com.android.tools.build.jetifier:jetifier-processor:1.0.0-beta08'
|
||||
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
670/5000
|
||||
Tusky v6.0
|
||||
|
||||
- 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 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
|
||||
- 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