Merge remote-tracking branch 'tuskyapp/develop'

This commit is contained in:
kyori19 2019-12-27 14:46:18 +09:00
commit 61bc887af5
111 changed files with 3915 additions and 3127 deletions

View File

@ -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", {

View File

@ -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

View File

@ -96,7 +96,7 @@
</activity>
<activity
android:name=".ComposeActivity"
android:name=".components.compose.ComposeActivity"
android:theme="@style/TuskyDialogActivityTheme"
android:windowSoftInputMode="stateVisible|adjustResize"/>
<activity

View File

@ -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

View File

@ -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) {

View File

@ -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;

View File

@ -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);
}

View File

@ -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)
}

View File

@ -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) {

View File

@ -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() {

View File

@ -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 )
}
}
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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

View File

@ -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
)

View File

@ -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();
}
}

View File

@ -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)
}
}
}
}

View File

@ -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
}
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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) {

View File

@ -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)

View File

@ -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");
}
};
}

View File

@ -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>
}

View File

@ -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?
)

View File

@ -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

View File

@ -35,7 +35,8 @@ import javax.inject.Singleton
ServicesModule::class,
BroadcastReceiverModule::class,
ViewModelModule::class,
RepositoryModule::class
RepositoryModule::class,
MediaUploaderModule::class
])
interface AppComponent {
@Component.Builder

View File

@ -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)
}

View File

@ -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)
}
}
}

View File

@ -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
}

View File

@ -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
)

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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")

View File

@ -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?

View File

@ -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)

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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
/**

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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))

View File

@ -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)

View File

@ -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;

View File

@ -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);

View File

@ -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)
}
})
}

View File

@ -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)
}

View File

@ -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;

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 &amp; 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 lapplication</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>

View File

@ -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>

View File

@ -325,9 +325,9 @@
<string name="unpin_action">Non fissare</string>
<string name="pin_action">Fissa</string>
<plurals name="favs">
<item quantity="one">&lt;b&gt;%1$s&lt;/b&gt; Mi piace</item>
<item quantity="other">&lt;b&gt;%1$s&lt;/b&gt; 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">&lt;b&gt;%s&lt;/b&gt; Boost</item>
<item quantity="other">&lt;b&gt;%s&lt;/b&gt; 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -368,9 +368,9 @@
<string name="pin_action">Przypnij do profilu</string>
<plurals name="favs">
<item quantity="one">&lt;b>%1$s&lt;/b&gt; polubienie</item>
<item quantity="few">&lt;b>%1$s&lt;/b&gt; polubienia</item>
<item quantity="many">&lt;b>%1$s&lt;/b&gt; polubień</item>
<item quantity="one">&lt;b&gt;%1$s&lt;/b&gt; polubienie</item>
<item quantity="few">&lt;b&gt;%1$s&lt;/b&gt; polubienia</item>
<item quantity="many">&lt;b&gt;%1$s&lt;/b&gt; 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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">&lt;b&gt;%1$s&lt;/b&gt; Favoriler</item>
</plurals>
<plurals name="reblogs">
<item quantity="one">&lt;b>%s&lt;/b> Yinelenen</item>
<item quantity="other">&lt;b>%s&lt;/b> Yinelenenler</item>
<item quantity="one">&lt;b&gt;%s&lt;/b&gt; Yinelenen</item>
<item quantity="other">&lt;b&gt;%s&lt;/b&gt; 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ı</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>

View File

@ -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" />

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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)))
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -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"
}
}

View File

@ -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

View File

@ -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 daltres petites millores i solucions derrors

View File

@ -0,0 +1,3 @@
Tusky v9.1
Aquesta versió garanteix la compatibilitat amb Mastodon 3 i millora el rendiment i l'estabilitat.

View File

@ -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

View File

@ -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.

View File

@ -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