* cleanup warnings, reorganize some code

* move ComposeAutoCompleteAdapter to compose package

* composeOptions doesn't need to be a class member

* add DraftsActivity and DraftsViewModel

* drafts

* remove unnecessary Unit in ComposeViewModel

* add schema/25.json

* fix db migration

* drafts

* cleanup code

* fix compose activity rotation bug

* fix media descriptions getting lost when restoring a draft

* improve deleting drafts

* fix ComposeActivityTest

* improve draft layout for almost empty drafts

* reformat code

* show toast when opening reply to deleted toot

* improve item_draft layout
This commit is contained in:
Konrad Pozniak 2021-01-21 18:57:09 +01:00 committed by GitHub
parent baa915a0a3
commit 940d6d395a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
85 changed files with 2032 additions and 381 deletions

View file

@ -67,6 +67,9 @@ android {
androidExtensions { androidExtensions {
experimental = true experimental = true
} }
buildFeatures {
viewBinding true
}
testOptions { testOptions {
unitTests { unitTests {
returnDefaultValues = true returnDefaultValues = true

View file

@ -0,0 +1,821 @@
{
"formatVersion": 1,
"database": {
"version": 25,
"identityHash": "e2cb844862443c2c5cc884c11f120d43",
"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": "DraftEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inReplyToId",
"columnName": "inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentWarning",
"columnName": "contentWarning",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "sensitive",
"columnName": "sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "failedToSend",
"columnName": "failedToSend",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "AccountEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "domain",
"columnName": "domain",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accessToken",
"columnName": "accessToken",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isActive",
"columnName": "isActive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profilePictureUrl",
"columnName": "profilePictureUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsEnabled",
"columnName": "notificationsEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsMentioned",
"columnName": "notificationsMentioned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowed",
"columnName": "notificationsFollowed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowRequested",
"columnName": "notificationsFollowRequested",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsReblogged",
"columnName": "notificationsReblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFavorited",
"columnName": "notificationsFavorited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsPolls",
"columnName": "notificationsPolls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsSubscriptions",
"columnName": "notificationsSubscriptions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationSound",
"columnName": "notificationSound",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationVibration",
"columnName": "notificationVibration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationLight",
"columnName": "notificationLight",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultPostPrivacy",
"columnName": "defaultPostPrivacy",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultMediaSensitivity",
"columnName": "defaultMediaSensitivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysShowSensitiveMedia",
"columnName": "alwaysShowSensitiveMedia",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysOpenSpoiler",
"columnName": "alwaysOpenSpoiler",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaPreviewEnabled",
"columnName": "mediaPreviewEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastNotificationId",
"columnName": "lastNotificationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activeNotifications",
"columnName": "activeNotifications",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tabPreferences",
"columnName": "tabPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsFilter",
"columnName": "notificationsFilter",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_AccountEntity_domain_accountId",
"unique": true,
"columnNames": [
"domain",
"accountId"
],
"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, `muted` INTEGER, 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
},
{
"fieldPath": "muted",
"columnName": "muted",
"affinity": "INTEGER",
"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, 'e2cb844862443c2c5cc884c11f120d43')"
]
}
}

View file

@ -146,6 +146,7 @@
<activity android:name=".components.instancemute.InstanceListActivity" /> <activity android:name=".components.instancemute.InstanceListActivity" />
<activity android:name=".components.scheduled.ScheduledTootActivity" /> <activity android:name=".components.scheduled.ScheduledTootActivity" />
<activity android:name=".components.announcements.AnnouncementsActivity" /> <activity android:name=".components.announcements.AnnouncementsActivity" />
<activity android:name=".components.drafts.DraftsActivity" />
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" /> <receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
<receiver <receiver

View file

@ -31,6 +31,7 @@ import android.widget.ImageView
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.edit
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat
import androidx.emoji.text.EmojiCompat.InitCallback import androidx.emoji.text.EmojiCompat.InitCallback
@ -52,11 +53,13 @@ import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
import com.keylesspalace.tusky.components.conversation.ConversationsRepository import com.keylesspalace.tusky.components.conversation.ConversationsRepository
import com.keylesspalace.tusky.components.drafts.DraftsActivity
import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.components.notifications.NotificationHelper
import com.keylesspalace.tusky.components.preference.PreferencesActivity import com.keylesspalace.tusky.components.preference.PreferencesActivity
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
import com.keylesspalace.tusky.components.search.SearchActivity import com.keylesspalace.tusky.components.search.SearchActivity
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.fragment.SFragment
import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.AccountSelectionListener
@ -98,6 +101,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
@Inject @Inject
lateinit var conversationRepository: ConversationsRepository lateinit var conversationRepository: ConversationsRepository
@Inject
lateinit var appDb: AppDatabase
private lateinit var header: AccountHeaderView private lateinit var header: AccountHeaderView
private var notificationTabPosition = 0 private var notificationTabPosition = 0
@ -229,6 +235,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
// Flush old media that was cached for sharing // Flush old media that was cached for sharing
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky")) deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
} }
draftWarning()
} }
override fun onResume() { override fun onResume() {
@ -397,7 +404,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
nameRes = R.string.action_access_saved_toot nameRes = R.string.action_access_saved_toot
iconRes = R.drawable.ic_notebook iconRes = R.drawable.ic_notebook
onClick = { onClick = {
val intent = Intent(context, SavedTootActivity::class.java) val intent = DraftsActivity.newIntent(context)
startActivityWithSlideInAnimation(intent) startActivityWithSlideInAnimation(intent)
} }
}, },
@ -741,6 +748,29 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
header.setActiveProfile(accountManager.activeAccount!!.id) header.setActiveProfile(accountManager.activeAccount!!.id)
} }
private fun draftWarning() {
val sharedPrefsKey = "show_draft_warning"
appDb.tootDao().savedTootCount()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
.subscribe { draftCount ->
val showDraftWarning = preferences.getBoolean(sharedPrefsKey, true)
if (draftCount > 0 && showDraftWarning) {
AlertDialog.Builder(this)
.setMessage(R.string.new_drafts_warning)
.setNegativeButton("Don't show again") { _, _ ->
preferences.edit(commit = true) {
putBoolean(sharedPrefsKey, false)
}
}
.setPositiveButton(android.R.string.ok, null)
.show()
}
}
}
override fun getActionButton(): FloatingActionButton? = composeButton override fun getActionButton(): FloatingActionButton? = composeButton
override fun androidInjector() = androidInjector override fun androidInjector() = androidInjector

View file

@ -89,7 +89,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
setSupportActionBar(toolbar); setSupportActionBar(toolbar);
ActionBar bar = getSupportActionBar(); ActionBar bar = getSupportActionBar();
if (bar != null) { if (bar != null) {
bar.setTitle(getString(R.string.title_saved_toot)); bar.setTitle(getString(R.string.title_drafts));
bar.setDisplayHomeAsUpEnabled(true); bar.setDisplayHomeAsUpEnabled(true);
bar.setDisplayShowHomeEnabled(true); bar.setDisplayShowHomeEnabled(true);
} }
@ -166,6 +166,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
ComposeOptions composeOptions = new ComposeOptions( ComposeOptions composeOptions = new ComposeOptions(
/*scheduledTootUid*/null, /*scheduledTootUid*/null,
item.getUid(), item.getUid(),
/*drafId*/null,
item.getText(), item.getText(),
jsonUrls, jsonUrls,
descriptions, descriptions,
@ -177,6 +178,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
item.getInReplyToUsername(), item.getInReplyToUsername(),
item.getInReplyToText(), item.getInReplyToText(),
/*mediaAttachments*/null, /*mediaAttachments*/null,
/*draftAttachments*/null,
/*scheduledAt*/null, /*scheduledAt*/null,
/*sensitive*/null, /*sensitive*/null,
/*poll*/null, /*poll*/null,

View file

@ -30,7 +30,6 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.provider.MediaStore import android.provider.MediaStore
import android.text.TextUtils
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.MenuItem import android.view.MenuItem
@ -57,13 +56,13 @@ import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.BuildConfig
import com.keylesspalace.tusky.R import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter
import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.EmojiAdapter
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.db.DraftAttachment
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
@ -81,7 +80,6 @@ import java.io.File
import java.io.IOException import java.io.IOException
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
import kotlin.collections.ArrayList
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@ -104,10 +102,10 @@ class ComposeActivity : BaseActivity(),
// this only exists when a status is trying to be sent, but uploads are still occurring // this only exists when a status is trying to be sent, but uploads are still occurring
private var finishingUploadDialog: ProgressDialog? = null private var finishingUploadDialog: ProgressDialog? = null
private var photoUploadUri: Uri? = null private var photoUploadUri: Uri? = null
@VisibleForTesting @VisibleForTesting
var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT
private var composeOptions: ComposeOptions? = null
private val viewModel: ComposeViewModel by viewModels { viewModelFactory } private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
private val maxUploadMediaNumber = 4 private val maxUploadMediaNumber = 4
@ -148,17 +146,17 @@ class ComposeActivity : BaseActivity(),
/* If the composer is started up as a reply to another post, override the "starting" state /* If the composer is started up as a reply to another post, override the "starting" state
* based on what the intent from the reply request passes. */ * based on what the intent from the reply request passes. */
if (intent != null) {
this.composeOptions = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA) val composeOptions: ComposeOptions? = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA)
viewModel.setup(composeOptions)
setupReplyViews(composeOptions?.replyingStatusAuthor) viewModel.setup(composeOptions)
val tootText = composeOptions?.tootText setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent)
if (!tootText.isNullOrEmpty()) { val tootText = composeOptions?.tootText
composeEditField.setText(tootText) if (!tootText.isNullOrEmpty()) {
} composeEditField.setText(tootText)
} }
if (!TextUtils.isEmpty(composeOptions?.scheduledAt)) { if (!composeOptions?.scheduledAt.isNullOrEmpty()) {
composeScheduleView.setDateTime(composeOptions?.scheduledAt) composeScheduleView.setDateTime(composeOptions?.scheduledAt)
} }
@ -169,38 +167,24 @@ class ComposeActivity : BaseActivity(),
viewModel.setupComplete.value = true viewModel.setupComplete.value = true
} }
private fun applyShareIntent(intent: Intent?, savedInstanceState: Bundle?) { private fun applyShareIntent(intent: Intent, savedInstanceState: Bundle?) {
if (intent != null && savedInstanceState == null) { if (savedInstanceState == null) {
/* Get incoming images being sent through a share action from another app. Only do this /* Get incoming images being sent through a share action from another app. Only do this
* when savedInstanceState is null, otherwise both the images from the intent and the * when savedInstanceState is null, otherwise both the images from the intent and the
* instance state will be re-queued. */ * instance state will be re-queued. */
val type = intent.type intent.type?.also { type ->
if (type != null) {
if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) { if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) {
val uriList = ArrayList<Uri>() when (intent.action) {
if (intent.action != null) { Intent.ACTION_SEND -> {
when (intent.action) { intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)?.let { uri ->
Intent.ACTION_SEND -> { pickMedia(uri)
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM) }
if (uri != null) { }
uriList.add(uri) Intent.ACTION_SEND_MULTIPLE -> {
} intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)?.forEach { uri ->
} pickMedia(uri)
Intent.ACTION_SEND_MULTIPLE -> {
val list = intent.getParcelableArrayListExtra<Uri>(
Intent.EXTRA_STREAM)
if (list != null) {
for (uri in list) {
if (uri != null) {
uriList.add(uri)
}
}
}
} }
} }
}
for (uri in uriList) {
pickMedia(uri)
} }
} else if (type == "text/plain" && intent.action == Intent.ACTION_SEND) { } else if (type == "text/plain" && intent.action == Intent.ACTION_SEND) {
@ -224,7 +208,7 @@ class ComposeActivity : BaseActivity(),
} }
} }
private fun setupReplyViews(replyingStatusAuthor: String?) { private fun setupReplyViews(replyingStatusAuthor: String?, replyingStatusContent: String?) {
if (replyingStatusAuthor != null) { if (replyingStatusAuthor != null) {
composeReplyView.show() composeReplyView.show()
composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor) composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor)
@ -248,7 +232,7 @@ class ComposeActivity : BaseActivity(),
} }
} }
} }
composeOptions?.replyingStatusContent?.let { composeReplyContentView.text = it } replyingStatusContent?.let { composeReplyContentView.text = it }
} }
private fun setupContentWarningField(startingContentWarning: String?) { private fun setupContentWarningField(startingContentWarning: String?) {
@ -651,7 +635,6 @@ class ComposeActivity : BaseActivity(),
} }
} }
private fun removePoll() { private fun removePoll() {
viewModel.poll.value = null viewModel.poll.value = null
pollPreview.hide() pollPreview.hide()
@ -835,22 +818,22 @@ class ComposeActivity : BaseActivity(),
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent) super.onActivityResult(requestCode, resultCode, intent)
if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) { if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) {
if(intent.data != null){ if (intent.data != null) {
// Single media, upload it and done. // Single media, upload it and done.
pickMedia(intent.data!!) pickMedia(intent.data!!)
}else if(intent.clipData != null){ } else if (intent.clipData != null) {
val clipData = intent.clipData!! val clipData = intent.clipData!!
val count = clipData.itemCount val count = clipData.itemCount
if(mediaCount + count > maxUploadMediaNumber){ if (mediaCount + count > maxUploadMediaNumber) {
// check if exist media + upcoming media > 4, then prob error message. // check if exist media + upcoming media > 4, then prob error message.
Toast.makeText(this, getString(R.string.error_upload_max_media_reached, maxUploadMediaNumber), Toast.LENGTH_SHORT).show() Toast.makeText(this, getString(R.string.error_upload_max_media_reached, maxUploadMediaNumber), Toast.LENGTH_SHORT).show()
}else{ } else {
// if not grater then 4, upload all multiple media. // if not grater then 4, upload all multiple media.
for (i in 0 until count) { for (i in 0 until count) {
val imageUri = clipData.getItemAt(i).getUri() val imageUri = clipData.getItemAt(i).getUri()
pickMedia(imageUri) pickMedia(imageUri)
}
} }
}
} }
} else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) { } else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) {
pickMedia(photoUploadUri!!) pickMedia(photoUploadUri!!)
@ -1018,8 +1001,9 @@ class ComposeActivity : BaseActivity(),
@Parcelize @Parcelize
data class ComposeOptions( data class ComposeOptions(
// Let's keep fields var until all consumers are Kotlin // Let's keep fields var until all consumers are Kotlin
var scheduledTootUid: String? = null, var scheduledTootId: String? = null,
var savedTootUid: Int? = null, var savedTootUid: Int? = null,
var draftId: Int? = null,
var tootText: String? = null, var tootText: String? = null,
var mediaUrls: List<String>? = null, var mediaUrls: List<String>? = null,
var mediaDescriptions: List<String>? = null, var mediaDescriptions: List<String>? = null,
@ -1031,6 +1015,7 @@ class ComposeActivity : BaseActivity(),
var replyingStatusAuthor: String? = null, var replyingStatusAuthor: String? = null,
var replyingStatusContent: String? = null, var replyingStatusContent: String? = null,
var mediaAttachments: List<Attachment>? = null, var mediaAttachments: List<Attachment>? = null,
var draftAttachments: List<DraftAttachment>? = null,
var scheduledAt: String? = null, var scheduledAt: String? = null,
var sensitive: Boolean? = null, var sensitive: Boolean? = null,
var poll: NewPoll? = null, var poll: NewPoll? = null,
@ -1057,7 +1042,6 @@ class ComposeActivity : BaseActivity(),
} }
} }
@JvmStatic
fun canHandleMimeType(mimeType: String?): Boolean { fun canHandleMimeType(mimeType: String?): Boolean {
return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain") return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain")
} }

View file

@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.adapter; package com.keylesspalace.tusky.components.compose;
import android.content.Context; import android.content.Context;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;

View file

@ -21,8 +21,8 @@ import androidx.core.net.toUri
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
import com.keylesspalace.tusky.components.drafts.DraftHelper
import com.keylesspalace.tusky.components.search.SearchType import com.keylesspalace.tusky.components.search.SearchType
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
@ -39,18 +39,12 @@ import io.reactivex.rxkotlin.Singles
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
/** class ComposeViewModel @Inject constructor(
* 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 api: MastodonApi,
private val accountManager: AccountManager, private val accountManager: AccountManager,
private val mediaUploader: MediaUploader, private val mediaUploader: MediaUploader,
private val serviceClient: ServiceClient, private val serviceClient: ServiceClient,
private val draftHelper: DraftHelper,
private val saveTootHelper: SaveTootHelper, private val saveTootHelper: SaveTootHelper,
private val db: AppDatabase private val db: AppDatabase
) : RxAwareViewModel() { ) : RxAwareViewModel() {
@ -59,7 +53,8 @@ class ComposeViewModel
private var replyingStatusContent: String? = null private var replyingStatusContent: String? = null
internal var startingText: String? = null internal var startingText: String? = null
private var savedTootUid: Int = 0 private var savedTootUid: Int = 0
private var scheduledTootUid: String? = null private var draftId: Int = 0
private var scheduledTootId: String? = null
private var startingContentWarning: String = "" private var startingContentWarning: String = ""
private var inReplyToId: String? = null private var inReplyToId: String? = null
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
@ -81,10 +76,6 @@ class ComposeViewModel
val markMediaAsSensitive = val markMediaAsSensitive =
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false) mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
fun toggleMarkSensitive() {
this.markMediaAsSensitive.value = !this.markMediaAsSensitive.value!!
}
val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN) val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN)
val showContentWarning = mutableLiveData(false) val showContentWarning = mutableLiveData(false)
val setupComplete = mutableLiveData(false) val setupComplete = mutableLiveData(false)
@ -96,7 +87,7 @@ class ComposeViewModel
private val mediaToDisposable = mutableMapOf<Long, Disposable>() private val mediaToDisposable = mutableMapOf<Long, Disposable>()
private val isEditingScheduledToot get() = !scheduledTootUid.isNullOrEmpty() private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty()
init { init {
@ -116,7 +107,7 @@ class ComposeViewModel
.onErrorResumeNext( .onErrorResumeNext(
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
) )
.subscribe ({ instanceEntity -> .subscribe({ instanceEntity ->
emoji.postValue(instanceEntity.emojiList) emoji.postValue(instanceEntity.emojiList)
instance.postValue(instanceEntity) instance.postValue(instanceEntity)
}, { throwable -> }, { throwable ->
@ -126,7 +117,7 @@ class ComposeViewModel
.autoDispose() .autoDispose()
} }
fun pickMedia(uri: Uri): LiveData<Either<Throwable, QueuedMedia>> { fun pickMedia(uri: Uri, description: String? = null): LiveData<Either<Throwable, QueuedMedia>> {
// We are not calling .toLiveData() here because we don't want to stop the process when // 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). // the Activity goes away temporarily (like on screen rotation).
val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>() val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>()
@ -138,7 +129,7 @@ class ComposeViewModel
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) { && mediaItems[0].type == QueuedMedia.Type.IMAGE) {
throw VideoOrImageException() throw VideoOrImageException()
} else { } else {
addMediaToQueue(type, uri, size) addMediaToQueue(type, uri, size, description)
} }
} }
.subscribe({ queuedMedia -> .subscribe({ queuedMedia ->
@ -150,12 +141,23 @@ class ComposeViewModel
return liveData return liveData
} }
private fun addMediaToQueue(type: QueuedMedia.Type, uri: Uri, mediaSize: Long): QueuedMedia { private fun addMediaToQueue(
val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, mediaSize) type: QueuedMedia.Type,
uri: Uri,
mediaSize: Long,
description: String? = null
): QueuedMedia {
val mediaItem = QueuedMedia(
localId = System.currentTimeMillis(),
uri = uri,
type = type,
mediaSize = mediaSize,
description = description
)
media.value = media.value!! + mediaItem media.value = media.value!! + mediaItem
mediaToDisposable[mediaItem.localId] = mediaUploader mediaToDisposable[mediaItem.localId] = mediaUploader
.uploadMedia(mediaItem) .uploadMedia(mediaItem)
.subscribe ({ event -> .subscribe({ event ->
val item = media.value?.find { it.localId == mediaItem.localId } val item = media.value?.find { it.localId == mediaItem.localId }
?: return@subscribe ?: return@subscribe
val newMediaItem = when (event) { val newMediaItem = when (event) {
@ -190,6 +192,10 @@ class ComposeViewModel
media.value = media.value!!.withoutFirstWhich { it.localId == item.localId } media.value = media.value!!.withoutFirstWhich { it.localId == item.localId }
} }
fun toggleMarkSensitive() {
this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true
}
fun didChange(content: String?, contentWarning: String?): Boolean { fun didChange(content: String?, contentWarning: String?): Boolean {
val textChanged = !(content.isNullOrEmpty() val textChanged = !(content.isNullOrEmpty()
@ -210,29 +216,37 @@ class ComposeViewModel
} }
fun deleteDraft() { fun deleteDraft() {
saveTootHelper.deleteDraft(this.savedTootUid) if (savedTootUid != 0) {
saveTootHelper.deleteDraft(savedTootUid)
}
if (draftId != 0) {
draftHelper.deleteDraftAndAttachments(draftId)
.subscribe()
}
} }
fun saveDraft(content: String, contentWarning: String) { fun saveDraft(content: String, contentWarning: String) {
val mediaUris = mutableListOf<String>()
val mediaDescriptions = mutableListOf<String?>() val mediaUris: MutableList<String> = mutableListOf()
for (item in media.value!!) { val mediaDescriptions: MutableList<String?> = mutableListOf()
media.value?.forEach { item ->
mediaUris.add(item.uri.toString()) mediaUris.add(item.uri.toString())
mediaDescriptions.add(item.description) mediaDescriptions.add(item.description)
} }
saveTootHelper.saveToot(
content, draftHelper.saveDraft(
contentWarning, draftId = draftId,
null, accountId = accountManager.activeAccount?.id!!,
mediaUris, inReplyToId = inReplyToId,
mediaDescriptions, content = content,
savedTootUid, contentWarning = contentWarning,
inReplyToId, sensitive = markMediaAsSensitive.value!!,
replyingStatusContent, visibility = statusVisibility.value!!,
replyingStatusAuthor, mediaUris = mediaUris,
statusVisibility.value!!, mediaDescriptions = mediaDescriptions,
poll.value poll = poll.value,
) failedToSend = false
).subscribe()
} }
/** /**
@ -246,7 +260,7 @@ class ComposeViewModel
): LiveData<Unit> { ): LiveData<Unit> {
val deletionObservable = if (isEditingScheduledToot) { val deletionObservable = if (isEditingScheduledToot) {
api.deleteScheduledStatus(scheduledTootUid.toString()).toObservable().map { Unit } api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { }
} else { } else {
just(Unit) just(Unit)
}.toLiveData() }.toLiveData()
@ -257,28 +271,30 @@ class ComposeViewModel
val mediaIds = ArrayList<String>() val mediaIds = ArrayList<String>()
val mediaUris = ArrayList<Uri>() val mediaUris = ArrayList<Uri>()
val mediaDescriptions = ArrayList<String>() val mediaDescriptions = ArrayList<String>()
val mediaTypes = ArrayList<QueuedMedia.Type>()
for (item in media.value!!) { for (item in media.value!!) {
mediaIds.add(item.id!!) mediaIds.add(item.id!!)
mediaUris.add(item.uri) mediaUris.add(item.uri)
mediaDescriptions.add(item.description ?: "") mediaDescriptions.add(item.description ?: "")
mediaTypes.add(item.type)
} }
val tootToSend = TootToSend( val tootToSend = TootToSend(
content, text = content,
spoilerText, warningText = spoilerText,
statusVisibility.value!!.serverString(), visibility = statusVisibility.value!!.serverString(),
mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!), sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
mediaIds, mediaIds = mediaIds,
mediaUris.map { it.toString() }, mediaUris = mediaUris.map { it.toString() },
mediaDescriptions, mediaDescriptions = mediaDescriptions,
scheduledAt = scheduledAt.value, scheduledAt = scheduledAt.value,
inReplyToId = inReplyToId, inReplyToId = inReplyToId,
poll = poll.value, poll = poll.value,
replyingStatusContent = null, replyingStatusContent = null,
replyingStatusAuthorUsername = null, replyingStatusAuthorUsername = null,
savedJsonUrls = null,
accountId = accountManager.activeAccount!!.id, accountId = accountManager.activeAccount!!.id,
savedTootUid = 0, savedTootUid = savedTootUid,
draftId = draftId,
idempotencyKey = randomAlphanumericString(16), idempotencyKey = randomAlphanumericString(16),
retries = 0 retries = 0
) )
@ -286,9 +302,7 @@ class ComposeViewModel
serviceClient.sendToot(tootToSend) serviceClient.sendToot(tootToSend)
} }
return combineLiveData(deletionObservable, sendObservable) { _, _ -> Unit } return combineLiveData(deletionObservable, sendObservable) { _, _ -> }
} }
fun updateDescription(localId: Long, description: String): LiveData<Boolean> { fun updateDescription(localId: Long, description: String): LiveData<Boolean> {
@ -319,7 +333,6 @@ class ComposeViewModel
return completedCaptioningLiveData return completedCaptioningLiveData
} }
fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> { fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
when (token[0]) { when (token[0]) {
'@' -> { '@' -> {
@ -370,14 +383,12 @@ class ComposeViewModel
} }
} }
override fun onCleared() {
for (uploadDisposable in mediaToDisposable.values) {
uploadDisposable.dispose()
}
super.onCleared()
}
fun setup(composeOptions: ComposeActivity.ComposeOptions?) { fun setup(composeOptions: ComposeActivity.ComposeOptions?) {
if (setupComplete.value == true) {
return
}
val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
@ -385,6 +396,7 @@ class ComposeViewModel
preferredVisibility.num.coerceAtLeast(replyVisibility.num)) preferredVisibility.num.coerceAtLeast(replyVisibility.num))
inReplyToId = composeOptions?.inReplyToId inReplyToId = composeOptions?.inReplyToId
modifiedInitialState = composeOptions?.modifiedInitialState == true modifiedInitialState = composeOptions?.modifiedInitialState == true
val contentWarning = composeOptions?.contentWarning val contentWarning = composeOptions?.contentWarning
@ -396,10 +408,11 @@ class ComposeViewModel
} }
// recreate media list // recreate media list
// when coming from SavedTootActivity
val loadedDraftMediaUris = composeOptions?.mediaUrls val loadedDraftMediaUris = composeOptions?.mediaUrls
val loadedDraftMediaDescriptions: List<String?>? = composeOptions?.mediaDescriptions val loadedDraftMediaDescriptions: List<String?>? = composeOptions?.mediaDescriptions
val draftAttachments = composeOptions?.draftAttachments
if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) { if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) {
// when coming from SavedTootActivity
loadedDraftMediaUris.zip(loadedDraftMediaDescriptions) loadedDraftMediaUris.zip(loadedDraftMediaDescriptions)
.forEach { (uri, description) -> .forEach { (uri, description) ->
pickMedia(uri.toUri()).observeForever { errorOrItem -> pickMedia(uri.toUri()).observeForever { errorOrItem ->
@ -408,23 +421,24 @@ class ComposeViewModel
} }
} }
} }
} else if (draftAttachments != null) {
// when coming from DraftActivity
draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) }
} else composeOptions?.mediaAttachments?.forEach { a -> } else composeOptions?.mediaAttachments?.forEach { a ->
// when coming from redraft // when coming from redraft or ScheduledTootActivity
val mediaType = when (a.type) { val mediaType = when (a.type) {
Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO
Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE
Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO
else -> QueuedMedia.Type.IMAGE
} }
addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description) addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description)
} }
savedTootUid = composeOptions?.savedTootUid ?: 0 savedTootUid = composeOptions?.savedTootUid ?: 0
scheduledTootUid = composeOptions?.scheduledTootUid draftId = composeOptions?.draftId ?: 0
scheduledTootId = composeOptions?.scheduledTootId
startingText = composeOptions?.tootText startingText = composeOptions?.tootText
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) { if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
startingVisibility = tootVisibility startingVisibility = tootVisibility
@ -441,7 +455,6 @@ class ComposeViewModel
startingText = builder.toString() startingText = builder.toString()
} }
scheduledAt.value = composeOptions?.scheduledAt scheduledAt.value = composeOptions?.scheduledAt
composeOptions?.sensitive?.let { markMediaAsSensitive.value = it } composeOptions?.sensitive?.let { markMediaAsSensitive.value = it }
@ -462,6 +475,13 @@ class ComposeViewModel
scheduledAt.value = newScheduledAt scheduledAt.value = newScheduledAt
} }
override fun onCleared() {
for (uploadDisposable in mediaToDisposable.values) {
uploadDisposable.dispose()
}
super.onCleared()
}
private companion object { private companion object {
const val TAG = "ComposeViewModel" const val TAG = "ComposeViewModel"
} }
@ -479,4 +499,9 @@ data class ComposeInstanceParams(
val pollMaxOptions: Int, val pollMaxOptions: Int,
val pollMaxLength: Int, val pollMaxLength: Int,
val supportsScheduled: Boolean val supportsScheduled: Boolean
) )
/**
* Thrown when trying to add an image when video is already present or the other way around
*/
class VideoOrImageException : Exception()

View file

@ -173,7 +173,13 @@ class MediaUploaderImpl(
val body = MultipartBody.Part.createFormData("file", filename, fileBody) val body = MultipartBody.Part.createFormData("file", filename, fileBody)
val uploadDisposable = mastodonApi.uploadMedia(body) val description = if (media.description != null) {
MultipartBody.Part.createFormData("description", media.description)
} else {
null
}
val uploadDisposable = mastodonApi.uploadMedia(body, description)
.subscribe({ attachment -> .subscribe({ attachment ->
emitter.onNext(UploadEvent.FinishedEvent(attachment)) emitter.onNext(UploadEvent.FinishedEvent(attachment))
emitter.onComplete() emitter.onComplete()

View file

@ -0,0 +1,159 @@
/* Copyright 2021 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.drafts
import android.content.Context
import android.net.Uri
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.db.AppDatabase
import com.keylesspalace.tusky.db.DraftAttachment
import com.keylesspalace.tusky.db.DraftEntity
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.util.IOUtils
import io.reactivex.Completable
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import javax.inject.Inject
class DraftHelper @Inject constructor(
val context: Context,
db: AppDatabase
) {
private val draftDao = db.draftDao()
fun saveDraft(
draftId: Int,
accountId: Long,
inReplyToId: String?,
content: String?,
contentWarning: String?,
sensitive: Boolean,
visibility: Status.Visibility,
mediaUris: List<String>,
mediaDescriptions: List<String?>,
poll: NewPoll?,
failedToSend: Boolean
): Completable {
return Single.fromCallable {
val draftDirectory = context.getExternalFilesDir("Tusky")
if (draftDirectory == null || !(draftDirectory.exists())) {
Log.e("DraftHelper", "Error obtaining directory to save media.")
throw Exception()
}
val uris = mediaUris.map { uriString ->
uriString.toUri()
}.map { uri ->
if (uri.isNotInFolder(draftDirectory)) {
uri.copyToFolder(draftDirectory)
} else {
uri
}
}
val types = uris.map { uri ->
val mimeType = context.contentResolver.getType(uri)
when (mimeType?.substring(0, mimeType.indexOf('/'))) {
"video" -> DraftAttachment.Type.VIDEO
"image" -> DraftAttachment.Type.IMAGE
"audio" -> DraftAttachment.Type.AUDIO
else -> throw IllegalStateException("unknown media type")
}
}
val attachments: MutableList<DraftAttachment> = mutableListOf()
for (i in mediaUris.indices) {
attachments.add(
DraftAttachment(
uriString = uris[i].toString(),
description = mediaDescriptions[i],
type = types[i]
)
)
}
DraftEntity(
id = draftId,
accountId = accountId,
inReplyToId = inReplyToId,
content = content,
contentWarning = contentWarning,
sensitive = sensitive,
visibility = visibility,
attachments = attachments,
poll = poll,
failedToSend = failedToSend
)
}.flatMapCompletable { draft ->
draftDao.insertOrReplace(draft)
}.subscribeOn(Schedulers.io())
}
fun deleteDraftAndAttachments(draftId: Int): Completable {
return draftDao.find(draftId)
.flatMapCompletable { draft ->
deleteDraftAndAttachments(draft)
}
}
fun deleteDraftAndAttachments(draft: DraftEntity): Completable {
return deleteAttachments(draft)
.andThen(draftDao.delete(draft.id))
}
fun deleteAttachments(draft: DraftEntity): Completable {
return Completable.fromCallable {
draft.attachments.forEach { attachment ->
if (context.contentResolver.delete(attachment.uri, null, null) == 0) {
Log.e("DraftHelper", "Did not delete file ${attachment.uriString}")
}
}
}.subscribeOn(Schedulers.io())
}
private fun Uri.isNotInFolder(folder: File): Boolean {
val filePath = path ?: return true
return File(filePath).parentFile == folder
}
private fun Uri.copyToFolder(folder: File): Uri {
val contentResolver = context.contentResolver
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
val mimeType = contentResolver.getType(this)
val map = MimeTypeMap.getSingleton()
val fileExtension = map.getExtensionFromMimeType(mimeType)
val filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension)
val file = File(folder, filename)
IOUtils.copyToFile(contentResolver, this, file)
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
}
}

View file

@ -0,0 +1,81 @@
/* Copyright 2020 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.drafts
import android.view.ViewGroup
import android.widget.ImageView
import androidx.appcompat.widget.AppCompatImageView
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
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.db.DraftAttachment
class DraftMediaAdapter(
private val attachmentClick: () -> Unit
) : ListAdapter<DraftAttachment, DraftMediaAdapter.DraftMediaViewHolder>(
object: DiffUtil.ItemCallback<DraftAttachment>() {
override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
return oldItem == newItem
}
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DraftMediaViewHolder {
return DraftMediaViewHolder(AppCompatImageView(parent.context))
}
override fun onBindViewHolder(holder: DraftMediaViewHolder, position: Int) {
getItem(position)?.let { attachment ->
if (attachment.type == DraftAttachment.Type.AUDIO) {
holder.imageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
} else {
Glide.with(holder.itemView.context)
.load(attachment.uri)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.dontAnimate()
.into(holder.imageView)
}
}
}
inner class DraftMediaViewHolder(val imageView: ImageView)
: RecyclerView.ViewHolder(imageView) {
init {
val thumbnailViewSize =
imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
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)
imageView.layoutParams = layoutParams
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
imageView.setOnClickListener {
attachmentClick()
}
}
}
}

View file

@ -0,0 +1,197 @@
/* Copyright 2020 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.drafts
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.widget.LinearLayout
import android.widget.Toast
import androidx.activity.viewModels
import androidx.lifecycle.Lifecycle
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.SavedTootActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.databinding.ActivityDraftsBinding
import com.keylesspalace.tusky.db.DraftEntity
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.uber.autodispose.android.lifecycle.autoDispose
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers
import retrofit2.HttpException
import javax.inject.Inject
class DraftsActivity : BaseActivity(), DraftActionListener {
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val viewModel: DraftsViewModel by viewModels { viewModelFactory }
private lateinit var binding: ActivityDraftsBinding
private lateinit var bottomSheet: BottomSheetBehavior<LinearLayout>
private var oldDraftsButton: MenuItem? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDraftsBinding.inflate(layoutInflater)
setContentView(binding.root)
setSupportActionBar(binding.includedToolbar.toolbar)
supportActionBar?.apply {
title = getString(R.string.title_drafts)
setDisplayHomeAsUpEnabled(true)
setDisplayShowHomeEnabled(true)
}
binding.draftsErrorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_saved_status)
val adapter = DraftsAdapter(this)
binding.draftsRecyclerView.adapter = adapter
binding.draftsRecyclerView.layoutManager = LinearLayoutManager(this)
binding.draftsRecyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root)
viewModel.drafts.observe(this) { draftList ->
if (draftList.isEmpty()) {
binding.draftsRecyclerView.hide()
binding.draftsErrorMessageView.show()
} else {
binding.draftsRecyclerView.show()
binding.draftsErrorMessageView.hide()
adapter.submitList(draftList)
}
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.drafts, menu)
oldDraftsButton = menu.findItem(R.id.action_old_drafts)
viewModel.showOldDraftsButton()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
.subscribe { showOldDraftsButton ->
oldDraftsButton?.isVisible = showOldDraftsButton
}
return true
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
onBackPressed()
return true
}
R.id.action_old_drafts -> {
val intent = Intent(this, SavedTootActivity::class.java)
startActivityWithSlideInAnimation(intent)
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onOpenDraft(draft: DraftEntity) {
if (draft.inReplyToId != null) {
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
viewModel.getToot(draft.inReplyToId)
.observeOn(AndroidSchedulers.mainThread())
.autoDispose(this)
.subscribe({ status ->
val composeOptions = ComposeActivity.ComposeOptions(
draftId = draft.id,
tootText = draft.content,
contentWarning = draft.contentWarning,
inReplyToId = draft.inReplyToId,
replyingStatusContent = status.content.toString(),
replyingStatusAuthor = status.account.localUsername,
draftAttachments = draft.attachments,
poll = draft.poll,
sensitive = draft.sensitive,
visibility = draft.visibility
)
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
startActivity(ComposeActivity.startIntent(this, composeOptions))
}, { throwable ->
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
Log.w(TAG, "failed loading reply information", throwable)
if (throwable is HttpException && throwable.code() == 404) {
// the original status to which a reply was drafted has been deleted
// let's open the ComposeActivity without reply information
Toast.makeText(this, getString(R.string.drafts_toot_reply_removed), Toast.LENGTH_LONG).show()
openDraftWithoutReply(draft)
} else {
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
.show()
}
})
} else {
openDraftWithoutReply(draft)
}
}
private fun openDraftWithoutReply(draft: DraftEntity) {
val composeOptions = ComposeActivity.ComposeOptions(
draftId = draft.id,
tootText = draft.content,
contentWarning = draft.contentWarning,
draftAttachments = draft.attachments,
poll = draft.poll,
sensitive = draft.sensitive,
visibility = draft.visibility
)
startActivity(ComposeActivity.startIntent(this, composeOptions))
}
override fun onDeleteDraft(draft: DraftEntity) {
viewModel.deleteDraft(draft)
Snackbar.make(binding.root, getString(R.string.draft_deleted), Snackbar.LENGTH_LONG)
.setAction(R.string.action_undo) {
viewModel.restoreDraft(draft)
}
.show()
}
companion object {
const val TAG = "DraftsActivity"
fun newIntent(context: Context) = Intent(context, DraftsActivity::class.java)
}
}

View file

@ -0,0 +1,92 @@
/* Copyright 2021 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.drafts
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.databinding.ItemDraftBinding
import com.keylesspalace.tusky.db.DraftEntity
import com.keylesspalace.tusky.util.BindingViewHolder
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.visible
interface DraftActionListener {
fun onOpenDraft(draft: DraftEntity)
fun onDeleteDraft(draft: DraftEntity)
}
class DraftsAdapter(
private val listener: DraftActionListener
) : PagedListAdapter<DraftEntity, BindingViewHolder<ItemDraftBinding>>(
object : DiffUtil.ItemCallback<DraftEntity>() {
override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
return oldItem.id == newItem.id
}
override fun areContentsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
return oldItem == newItem
}
}
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder<ItemDraftBinding> {
val binding = ItemDraftBinding.inflate(LayoutInflater.from(parent.context), parent, false)
val viewHolder = BindingViewHolder(binding)
binding.draftMediaPreview.layoutManager = LinearLayoutManager(binding.root.context, RecyclerView.HORIZONTAL, false)
binding.draftMediaPreview.adapter = DraftMediaAdapter {
getItem(viewHolder.adapterPosition)?.let { draft ->
listener.onOpenDraft(draft)
}
}
return viewHolder
}
override fun onBindViewHolder(holder: BindingViewHolder<ItemDraftBinding>, position: Int) {
getItem(position)?.let { draft ->
holder.binding.root.setOnClickListener {
listener.onOpenDraft(draft)
}
holder.binding.deleteButton.setOnClickListener {
listener.onDeleteDraft(draft)
}
holder.binding.draftSendingInfo.visible(draft.failedToSend)
holder.binding.contentWarning.visible(!draft.contentWarning.isNullOrEmpty())
holder.binding.contentWarning.text = draft.contentWarning
holder.binding.content.text = draft.content
holder.binding.draftMediaPreview.visible(draft.attachments.isNotEmpty())
(holder.binding.draftMediaPreview.adapter as DraftMediaAdapter).submitList(draft.attachments)
if (draft.poll != null) {
holder.binding.draftPoll.show()
holder.binding.draftPoll.setPoll(draft.poll)
} else {
holder.binding.draftPoll.hide()
}
}
}
}

View file

@ -0,0 +1,69 @@
/* Copyright 2020 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.components.drafts
import androidx.lifecycle.ViewModel
import androidx.paging.toLiveData
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.db.DraftEntity
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import io.reactivex.Observable
import io.reactivex.Single
import javax.inject.Inject
class DraftsViewModel @Inject constructor(
val database: AppDatabase,
val accountManager: AccountManager,
val api: MastodonApi,
val draftHelper: DraftHelper
) : ViewModel() {
val drafts = database.draftDao().loadDrafts(accountManager.activeAccount?.id!!).toLiveData(pageSize = 20)
private val deletedDrafts: MutableList<DraftEntity> = mutableListOf()
fun showOldDraftsButton(): Observable<Boolean> {
return database.tootDao().savedTootCount()
.map { count -> count > 0 }
}
fun deleteDraft(draft: DraftEntity) {
// this does not immediately delete media files to avoid unnecessary file operations
// in case the user decides to restore the draft
database.draftDao().delete(draft.id)
.subscribe()
deletedDrafts.add(draft)
}
fun restoreDraft(draft: DraftEntity) {
database.draftDao().insertOrReplace(draft)
.subscribe()
deletedDrafts.remove(draft)
}
fun getToot(tootId: String): Single<Status> {
return api.statusSingle(tootId)
}
override fun onCleared() {
deletedDrafts.forEach {
draftHelper.deleteAttachments(it).subscribe()
}
}
}

View file

@ -120,7 +120,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
override fun edit(item: ScheduledStatus) { override fun edit(item: ScheduledStatus) {
val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions( val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions(
scheduledTootUid = item.id, scheduledTootId = item.id,
tootText = item.params.text, tootText = item.params.text,
contentWarning = item.params.spoilerText, contentWarning = item.params.spoilerText,
mediaAttachments = item.mediaAttachments, mediaAttachments = item.mediaAttachments,

View file

@ -28,9 +28,9 @@ import com.keylesspalace.tusky.components.conversation.ConversationEntity;
* DB version & declare DAO * DB version & declare DAO
*/ */
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, @Database(entities = { TootEntity.class, DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class TimelineAccountEntity.class, ConversationEntity.class
}, version = 24) }, version = 25)
public abstract class AppDatabase extends RoomDatabase { public abstract class AppDatabase extends RoomDatabase {
public abstract TootDao tootDao(); public abstract TootDao tootDao();
@ -38,6 +38,7 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract InstanceDao instanceDao(); public abstract InstanceDao instanceDao();
public abstract ConversationsDao conversationDao(); public abstract ConversationsDao conversationDao();
public abstract TimelineDao timelineDao(); public abstract TimelineDao timelineDao();
public abstract DraftDao draftDao();
public static final Migration MIGRATION_2_3 = new Migration(2, 3) { public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override @Override
@ -46,7 +47,6 @@ public abstract class AppDatabase extends RoomDatabase {
database.execSQL("INSERT INTO TootEntity2 SELECT * FROM TootEntity;"); database.execSQL("INSERT INTO TootEntity2 SELECT * FROM TootEntity;");
database.execSQL("DROP TABLE TootEntity;"); database.execSQL("DROP TABLE TootEntity;");
database.execSQL("ALTER TABLE TootEntity2 RENAME TO TootEntity;"); database.execSQL("ALTER TABLE TootEntity2 RENAME TO TootEntity;");
} }
}; };
@ -347,4 +347,22 @@ public abstract class AppDatabase extends RoomDatabase {
} }
}; };
public static final Migration MIGRATION_24_25 = new Migration(24, 25) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL(
"CREATE TABLE IF NOT EXISTS `DraftEntity` (" +
"`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
"`accountId` INTEGER NOT NULL, " +
"`inReplyToId` TEXT," +
"`content` TEXT," +
"`contentWarning` TEXT," +
"`sensitive` INTEGER NOT NULL," +
"`visibility` INTEGER NOT NULL," +
"`attachments` TEXT NOT NULL," +
"`poll` TEXT," +
"`failedToSend` INTEGER NOT NULL)"
);
}
};
} }

View file

@ -24,10 +24,7 @@ import com.google.gson.reflect.TypeToken
import com.keylesspalace.tusky.TabData import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
import com.keylesspalace.tusky.createTabDataFromId import com.keylesspalace.tusky.createTabDataFromId
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.*
import com.keylesspalace.tusky.entity.Emoji
import com.keylesspalace.tusky.entity.Poll
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.json.SpannedTypeAdapter
import com.keylesspalace.tusky.util.trimTrailingWhitespace import com.keylesspalace.tusky.util.trimTrailingWhitespace
import java.net.URLDecoder import java.net.URLDecoder
@ -151,4 +148,23 @@ class Converters {
return gson.fromJson(pollJson, Poll::class.java) return gson.fromJson(pollJson, Poll::class.java)
} }
} @TypeConverter
fun newPollToJson(newPoll: NewPoll?): String? {
return gson.toJson(newPoll)
}
@TypeConverter
fun jsonToNewPoll(newPollJson: String?): NewPoll? {
return gson.fromJson(newPollJson, NewPoll::class.java)
}
@TypeConverter
fun draftAttachmentListToJson(draftAttachments: List<DraftAttachment>?): String? {
return gson.toJson(draftAttachments)
}
@TypeConverter
fun jsonToDraftAttachmentList(draftAttachmentListJson: String?): List<DraftAttachment>? {
return gson.fromJson(draftAttachmentListJson, object : TypeToken<List<DraftAttachment>>() {}.type)
}
}

View file

@ -0,0 +1,40 @@
/* Copyright 2020 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.db
import androidx.paging.DataSource
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.reactivex.Completable
import io.reactivex.Single
@Dao
interface DraftDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertOrReplace(draft: DraftEntity): Completable
@Query("SELECT * FROM DraftEntity WHERE accountId = :accountId ORDER BY id ASC")
fun loadDrafts(accountId: Long): DataSource.Factory<Int, DraftEntity>
@Query("DELETE FROM DraftEntity WHERE id = :id")
fun delete(id: Int): Completable
@Query("SELECT * FROM DraftEntity WHERE id = :id")
fun find(id: Int): Single<DraftEntity?>
}

View file

@ -0,0 +1,55 @@
/* Copyright 2020 Tusky Contributors
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.db
import android.net.Uri
import android.os.Parcelable
import androidx.core.net.toUri
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.keylesspalace.tusky.entity.NewPoll
import com.keylesspalace.tusky.entity.Status
import kotlinx.android.parcel.Parcelize
@Entity
@TypeConverters(Converters::class)
data class DraftEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val accountId: Long,
val inReplyToId: String?,
val content: String?,
val contentWarning: String?,
val sensitive: Boolean,
val visibility: Status.Visibility,
val attachments: List<DraftAttachment>,
val poll: NewPoll?,
val failedToSend: Boolean
)
@Parcelize
data class DraftAttachment(
val uriString: String,
val description: String?,
val type: Type
): Parcelable {
val uri: Uri
get() = uriString.toUri()
enum class Type {
IMAGE, VIDEO, AUDIO;
}
}

View file

@ -26,7 +26,7 @@ import com.keylesspalace.tusky.entity.Status
// Avoiding rescanning status table when accounts table changes. Recommended by Room(c). // Avoiding rescanning status table when accounts table changes. Recommended by Room(c).
indices = [Index("authorServerId", "timelineUserId")] indices = [Index("authorServerId", "timelineUserId")]
) )
@TypeConverters(TootEntity.Converters::class) @TypeConverters(Converters::class)
data class TimelineStatusEntity( data class TimelineStatusEntity(
val serverId: String, // id never flips: we need it for sorting so it's a real id val serverId: String, // id never flips: we need it for sorting so it's a real id
val url: String?, val url: String?,

View file

@ -16,12 +16,12 @@
package com.keylesspalace.tusky.db; package com.keylesspalace.tusky.db;
import androidx.room.Dao; import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query; import androidx.room.Query;
import java.util.List; import java.util.List;
import io.reactivex.Observable;
/** /**
* Created by cto3543 on 28/06/2017. * Created by cto3543 on 28/06/2017.
* *
@ -30,8 +30,6 @@ import java.util.List;
@Dao @Dao
public interface TootDao { public interface TootDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertOrReplace(TootEntity users);
@Query("SELECT * FROM TootEntity ORDER BY uid DESC") @Query("SELECT * FROM TootEntity ORDER BY uid DESC")
List<TootEntity> loadAll(); List<TootEntity> loadAll();
@ -41,4 +39,7 @@ public interface TootDao {
@Query("SELECT * FROM TootEntity WHERE uid = :uid") @Query("SELECT * FROM TootEntity WHERE uid = :uid")
TootEntity find(int uid); TootEntity find(int uid);
}
@Query("SELECT COUNT(*) FROM TootEntity")
Observable<Integer> savedTootCount();
}

View file

@ -18,6 +18,7 @@ package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.* import com.keylesspalace.tusky.*
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.drafts.DraftsActivity
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
import com.keylesspalace.tusky.components.preference.PreferencesActivity import com.keylesspalace.tusky.components.preference.PreferencesActivity
import com.keylesspalace.tusky.components.report.ReportActivity import com.keylesspalace.tusky.components.report.ReportActivity
@ -107,4 +108,7 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector @ContributesAndroidInjector
abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity
@ContributesAndroidInjector
abstract fun contributesDraftActivity(): DraftsActivity
} }

View file

@ -80,7 +80,7 @@ class AppModule {
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16, AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22, AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24) AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25)
.build() .build()
} }

View file

@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider
import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel
import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.compose.ComposeViewModel
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
import com.keylesspalace.tusky.components.drafts.DraftsViewModel
import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel
import com.keylesspalace.tusky.components.search.SearchViewModel import com.keylesspalace.tusky.components.search.SearchViewModel
@ -91,5 +92,10 @@ abstract class ViewModelModule {
@ViewModelKey(AnnouncementsViewModel::class) @ViewModelKey(AnnouncementsViewModel::class)
internal abstract fun announcementsViewModel(viewModel: AnnouncementsViewModel): ViewModel internal abstract fun announcementsViewModel(viewModel: AnnouncementsViewModel): ViewModel
@Binds
@IntoMap
@ViewModelKey(DraftsViewModel::class)
internal abstract fun draftsViewModel(viewModel: DraftsViewModel): ViewModel
//Add more ViewModels here //Add more ViewModels here
} }

View file

@ -124,7 +124,8 @@ interface MastodonApi {
@Multipart @Multipart
@POST("api/v1/media") @POST("api/v1/media")
fun uploadMedia( fun uploadMedia(
@Part file: MultipartBody.Part @Part file: MultipartBody.Part,
@Part description: MultipartBody.Part? = null
): Single<Attachment> ): Single<Attachment>
@FormUrlEncoded @FormUrlEncoded
@ -147,6 +148,11 @@ interface MastodonApi {
@Path("id") statusId: String @Path("id") statusId: String
): Call<Status> ): Call<Status>
@GET("api/v1/statuses/{id}")
fun statusSingle(
@Path("id") statusId: String
): Single<Status>
@GET("api/v1/statuses/{id}/context") @GET("api/v1/statuses/{id}/context")
fun statusContext( fun statusContext(
@Path("id") statusId: String @Path("id") statusId: String

View file

@ -60,7 +60,6 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
val notificationManager = NotificationManagerCompat.from(context) val notificationManager = NotificationManagerCompat.from(context)
if (intent.action == NotificationHelper.REPLY_ACTION) { if (intent.action == NotificationHelper.REPLY_ACTION) {
val message = getReplyMessage(intent) val message = getReplyMessage(intent)
@ -89,22 +88,23 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
val sendIntent = SendTootService.sendTootIntent( val sendIntent = SendTootService.sendTootIntent(
context, context,
TootToSend( TootToSend(
text, text = text,
spoiler, warningText = spoiler,
visibility.serverString(), visibility = visibility.serverString(),
false, sensitive = false,
emptyList(), mediaIds = emptyList(),
emptyList(), mediaUris = emptyList(),
emptyList(), mediaDescriptions = emptyList(),
null, scheduledAt = null,
citedStatusId, inReplyToId = citedStatusId,
null, poll = null,
null, replyingStatusContent = null,
null, replyingStatusAuthorUsername = null,
null, account.id, accountId = account.id,
0, savedTootUid = -1,
randomAlphanumericString(16), draftId = -1,
0 idempotencyKey = randomAlphanumericString(16),
retries = 0
) )
) )

View file

@ -18,6 +18,7 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.StatusComposedEvent import com.keylesspalace.tusky.appstore.StatusComposedEvent
import com.keylesspalace.tusky.appstore.StatusScheduledEvent import com.keylesspalace.tusky.appstore.StatusScheduledEvent
import com.keylesspalace.tusky.components.drafts.DraftHelper
import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.Injectable
@ -46,7 +47,8 @@ class SendTootService : Service(), Injectable {
lateinit var eventHub: EventHub lateinit var eventHub: EventHub
@Inject @Inject
lateinit var database: AppDatabase lateinit var database: AppDatabase
@Inject
lateinit var draftHelper: DraftHelper
@Inject @Inject
lateinit var saveTootHelper: SaveTootHelper lateinit var saveTootHelper: SaveTootHelper
@ -163,6 +165,10 @@ class SendTootService : Service(), Injectable {
if (tootToSend.savedTootUid != 0) { if (tootToSend.savedTootUid != 0) {
saveTootHelper.deleteDraft(tootToSend.savedTootUid) saveTootHelper.deleteDraft(tootToSend.savedTootUid)
} }
if (tootToSend.draftId != 0) {
draftHelper.deleteDraftAndAttachments(tootToSend.draftId)
.subscribe()
}
if (scheduled) { if (scheduled) {
response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch) response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch)
@ -245,17 +251,19 @@ class SendTootService : Service(), Injectable {
private fun saveTootToDrafts(toot: TootToSend) { private fun saveTootToDrafts(toot: TootToSend) {
saveTootHelper.saveToot(toot.text, draftHelper.saveDraft(
toot.warningText, draftId = toot.draftId,
toot.savedJsonUrls, accountId = toot.accountId,
toot.mediaUris, inReplyToId = toot.inReplyToId,
toot.mediaDescriptions, content = toot.text,
toot.savedTootUid, contentWarning = toot.warningText,
toot.inReplyToId, sensitive = toot.sensitive,
toot.replyingStatusContent, visibility = Status.Visibility.byString(toot.visibility),
toot.replyingStatusAuthorUsername, mediaUris = toot.mediaUris,
Status.Visibility.byString(toot.visibility), mediaDescriptions = toot.mediaDescriptions,
toot.poll) poll = toot.poll,
failedToSend = true
).subscribe()
} }
private fun cancelSendingIntent(tootId: Int): PendingIntent { private fun cancelSendingIntent(tootId: Int): PendingIntent {
@ -323,9 +331,9 @@ data class TootToSend(
val poll: NewPoll?, val poll: NewPoll?,
val replyingStatusContent: String?, val replyingStatusContent: String?,
val replyingStatusAuthorUsername: String?, val replyingStatusAuthorUsername: String?,
val savedJsonUrls: List<String>?,
val accountId: Long, val accountId: Long,
val savedTootUid: Int, val savedTootUid: Int,
val draftId: Int,
val idempotencyKey: String, val idempotencyKey: String,
var retries: Int var retries: Int
) : Parcelable ) : Parcelable

View file

@ -0,0 +1,8 @@
package com.keylesspalace.tusky.util
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
class BindingViewHolder<T : ViewBinding>(
val binding: T
) : RecyclerView.ViewHolder(binding.root)

View file

@ -1,5 +1,6 @@
package com.keylesspalace.tusky.util package com.keylesspalace.tusky.util
import androidx.annotation.CallSuper
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
@ -9,6 +10,7 @@ open class RxAwareViewModel : ViewModel() {
fun Disposable.autoDispose() = disposables.add(this) fun Disposable.autoDispose() = disposables.add(this)
@CallSuper
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
disposables.clear() disposables.clear()

View file

@ -1,33 +1,18 @@
package com.keylesspalace.tusky.util; package com.keylesspalace.tusky.util;
import android.annotation.SuppressLint;
import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.FileProvider;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import com.keylesspalace.tusky.BuildConfig;
import com.keylesspalace.tusky.db.AppDatabase; import com.keylesspalace.tusky.db.AppDatabase;
import com.keylesspalace.tusky.db.TootDao; import com.keylesspalace.tusky.db.TootDao;
import com.keylesspalace.tusky.db.TootEntity; import com.keylesspalace.tusky.db.TootEntity;
import com.keylesspalace.tusky.entity.NewPoll;
import com.keylesspalace.tusky.entity.Status;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject; import javax.inject.Inject;
@ -45,61 +30,6 @@ public final class SaveTootHelper {
this.context = context; this.context = context;
} }
@SuppressLint("StaticFieldLeak")
public boolean saveToot(@NonNull String content,
@NonNull String contentWarning,
@Nullable List<String> savedJsonUrls,
@NonNull List<String> mediaUris,
@NonNull List<String> mediaDescriptions,
int savedTootUid,
@Nullable String inReplyToId,
@Nullable String replyingStatusContent,
@Nullable String replyingStatusAuthorUsername,
@NonNull Status.Visibility statusVisibility,
@Nullable NewPoll poll) {
if (TextUtils.isEmpty(content) && mediaUris.isEmpty() && poll == null) {
return false;
}
// Get any existing file's URIs.
String mediaUrlsSerialized = null;
String mediaDescriptionsSerialized = null;
if (!ListUtils.isEmpty(mediaUris)) {
List<String> savedList = saveMedia(mediaUris, savedJsonUrls);
if (!ListUtils.isEmpty(savedList)) {
mediaUrlsSerialized = gson.toJson(savedList);
if (!ListUtils.isEmpty(savedJsonUrls)) {
deleteMedia(setDifference(savedJsonUrls, savedList));
}
} else {
return false;
}
mediaDescriptionsSerialized = gson.toJson(mediaDescriptions);
} 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(savedJsonUrls);
}
final TootEntity toot = new TootEntity(savedTootUid, content, mediaUrlsSerialized, mediaDescriptionsSerialized, contentWarning,
inReplyToId,
replyingStatusContent,
replyingStatusAuthorUsername,
statusVisibility,
poll);
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
tootDao.insertOrReplace(toot);
return null;
}
}.execute();
return true;
}
public void deleteDraft(int tootId) { public void deleteDraft(int tootId) {
TootEntity item = tootDao.find(tootId); TootEntity item = tootDao.find(tootId);
if (item != null) { if (item != null) {
@ -124,82 +54,4 @@ public final class SaveTootHelper {
tootDao.delete(item.getUid()); tootDao.delete(item.getUid());
} }
@Nullable }
private List<String> saveMedia(@NonNull List<String> mediaUris,
@Nullable List<String> existingUris) {
File directory = context.getExternalFilesDir("Tusky");
if (directory == null || !(directory.exists())) {
Log.e(TAG, "Error obtaining directory to save media.");
return null;
}
ContentResolver contentResolver = context.getContentResolver();
ArrayList<File> filesSoFar = new ArrayList<>();
ArrayList<String> results = new ArrayList<>();
for (String mediaUri : mediaUris) {
/* If the media was already saved in a previous draft, there's no need to save another
* copy, just add the existing URI to the results. */
if (existingUris != null) {
int index = existingUris.indexOf(mediaUri);
if (index != -1) {
results.add(mediaUri);
continue;
}
}
// Otherwise, save the media.
Uri uri = Uri.parse(mediaUri);
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
String mimeType = contentResolver.getType(uri);
MimeTypeMap map = MimeTypeMap.getSingleton();
String fileExtension = map.getExtensionFromMimeType(mimeType);
String filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension);
File file = new File(directory, filename);
filesSoFar.add(file);
boolean copied = IOUtils.copyToFile(contentResolver, uri, file);
if (!copied) {
/* If any media files were created in prior iterations, delete those before
* returning. */
for (File earlierFile : filesSoFar) {
boolean deleted = earlierFile.delete();
if (!deleted) {
Log.i(TAG, "Could not delete the file " + earlierFile.toString());
}
}
return null;
}
Uri resultUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file);
results.add(resultUri.toString());
}
return results;
}
private void deleteMedia(List<String> mediaUris) {
for (String uriString : mediaUris) {
Uri uri = Uri.parse(uriString);
if (context.getContentResolver().delete(uri, null, null) == 0) {
Log.e(TAG, String.format("Did not delete file %s.", uriString));
}
}
}
/**
* AB={xA|xB}
*
* @return all elements of set A that are not in set B.
*/
private static List<String> setDifference(List<String> a, List<String> b) {
List<String> c = new ArrayList<>();
for (String s : a) {
if (!b.contains(s)) {
c.add(s);
}
}
return c;
}
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M11,15H13V17H11V15M11,7H13V13H11V7M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20Z" />
</vector>

View file

@ -4,5 +4,5 @@
android:width="24dp" android:width="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M3,7V5H5V4C5,2.89 5.9,2 7,2H13V9L15.5,7.5L18,9V2H19C20.05,2 21,2.95 21,4V20C21,21.05 20.05,22 19,22H7C5.95,22 5,21.05 5,20V19H3V17H5V13H3V11H5V7H3M7,11H5V13H7V11M7,7V5H5V7H7M7,19V17H5V19H7Z" /> <path android:fillColor="?attr/colorControlNormal" android:pathData="M3,7V5H5V4C5,2.89 5.9,2 7,2H13V9L15.5,7.5L18,9V2H19C20.05,2 21,2.95 21,4V20C21,21.05 20.05,22 19,22H7C5.95,22 5,21.05 5,20V19H3V17H5V13H3V11H5V7H3M7,11H5V13H7V11M7,7V5H5V7H7M7,19V17H5V19H7Z" />
</vector> </vector>

View file

@ -1,8 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="?attr/windowBackgroundColor"> android:background="?attr/windowBackgroundColor"
tools:viewBindingIgnore="true">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshLayout" android:id="@+id/swipeRefreshLayout"

View file

@ -145,9 +145,12 @@
android:id="@+id/pollPreview" android:id="@+id/pollPreview"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minWidth="@dimen/poll_preview_min_width"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible" /> tools:visibility="visible" />
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>
<LinearLayout <LinearLayout

View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".components.drafts.DraftsActivity">
<include
android:id="@+id/includedToolbar"
layout="@layout/toolbar_basic" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/draftsRecyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
<com.keylesspalace.tusky.view.BackgroundMessageView
android:id="@+id/draftsErrorMessageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@android:color/transparent"
android:visibility="gone"
android:layout_gravity="center"
tools:src="@drawable/elephant_error"
tools:visibility="visible" />
<include
android:id="@+id/bottomSheet"
layout="@layout/item_status_bottom_sheet" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,9 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/swipeRefreshLayout" android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="top"> android:layout_gravity="top"
tools:viewBindingIgnore="true">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView" android:id="@+id/recyclerView"

View file

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:background="?attr/selectableItemBackground"
android:paddingTop="4dp"
android:paddingBottom="4dp">
<TextView
android:id="@+id/draftSendingInfo"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:drawablePadding="4dp"
android:fontFamily="sans-serif-medium"
android:gravity="center_vertical"
android:text="@string/drafts_toot_failed_to_send"
android:textColor="@color/tusky_red"
android:textSize="?attr/status_text_medium"
app:drawableStartCompat="@drawable/ic_alert_circle"
app:drawableTint="@color/tusky_red"
app:layout_constraintEnd_toStartOf="@id/deleteButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/contentWarning"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:fontFamily="sans-serif-medium"
android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toStartOf="@id/deleteButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/draftSendingInfo"
tools:text="Some content warning" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginEnd="8dp"
android:textSize="?attr/status_text_medium"
app:layout_constraintEnd_toStartOf="@id/deleteButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/contentWarning"
tools:text="Some toot content. May be very long." />
<ImageButton
android:id="@+id/deleteButton"
style="@style/TuskyImageButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="center_vertical"
android:layout_margin="12dp"
android:contentDescription="@string/action_delete"
android:padding="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0"
app:srcCompat="@drawable/ic_clear_24dp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/draftMediaPreview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/content" />
<com.keylesspalace.tusky.components.compose.view.PollPreviewView
android:id="@+id/draftPoll"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:minWidth="@dimen/poll_preview_min_width"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/draftMediaPreview"
app:layout_goneMarginEnd="8dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,19 +1,16 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.appbar.AppBarLayout
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="@dimen/actionbar_elevation"
app:layout_collapseMode="pin">
<com.google.android.material.appbar.AppBarLayout <androidx.appcompat.widget.Toolbar
android:id="@+id/appbar" android:id="@+id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="?attr/actionBarSize" />
android:elevation="@dimen/actionbar_elevation"
app:layout_collapseMode="pin">
<androidx.appcompat.widget.Toolbar </com.google.android.material.appbar.AppBarLayout>
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
</merge>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_old_drafts"
android:icon="@drawable/ic_notebook"
android:title="@string/old_drafts"
android:visible="false"
app:showAsAction="ifRoom" />
</menu>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">الحسابات المحظورة</string> <string name="title_blocks">الحسابات المحظورة</string>
<string name="title_follow_requests">طلبات المتابعة</string> <string name="title_follow_requests">طلبات المتابعة</string>
<string name="title_edit_profile">عدل ملفك التعريفي</string> <string name="title_edit_profile">عدل ملفك التعريفي</string>
<string name="title_saved_toot">المسودات</string> <string name="title_drafts">المسودات</string>
<string name="title_licenses">الرّخص</string> <string name="title_licenses">الرّخص</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">شارَكَه %s</string> <string name="status_boosted_format">شارَكَه %s</string>

View file

@ -16,7 +16,7 @@
<string name="action_search">ⵏⴰⴸⵉ</string> <string name="action_search">ⵏⴰⴸⵉ</string>
<string name="action_edit_profile">ⵣⵔⴻⴳ ⴰⵎⴰⵖⵏⵓ</string> <string name="action_edit_profile">ⵣⵔⴻⴳ ⴰⵎⴰⵖⵏⵓ</string>
<string name="action_logout">ⴼⴼⴻⵖ</string> <string name="action_logout">ⴼⴼⴻⵖ</string>
<string name="title_saved_toot">ⵉⵔⴻⵡⵡⴰⵢⴻⵏ</string> <string name="title_drafts">ⵉⵔⴻⵡⵡⴰⵢⴻⵏ</string>
<string name="title_favourites">ⵉⵙⵎⴻⵏⵢⵉⴼⴻⵏ</string> <string name="title_favourites">ⵉⵙⵎⴻⵏⵢⵉⴼⴻⵏ</string>
<string name="button_back">ⵓⵖⴰⵍ</string> <string name="button_back">ⵓⵖⴰⵍ</string>
<string name="button_continue">ⴽⴻⵎⵎⴻⵍ</string> <string name="button_continue">ⴽⴻⵎⵎⴻⵍ</string>

View file

@ -292,7 +292,7 @@
<string name="status_media_hidden_title">মিডিয়া লুকানো</string> <string name="status_media_hidden_title">মিডিয়া লুকানো</string>
<string name="status_sensitive_media_title">সংবেদনশীল কন্টেন্ট</string> <string name="status_sensitive_media_title">সংবেদনশীল কন্টেন্ট</string>
<string name="title_licenses">লাইসেন্সগুলি</string> <string name="title_licenses">লাইসেন্সগুলি</string>
<string name="title_saved_toot">খসড়াগুলো</string> <string name="title_drafts">খসড়াগুলো</string>
<string name="title_edit_profile">আপনার প্রোফাইল সম্পাদনা করুন</string> <string name="title_edit_profile">আপনার প্রোফাইল সম্পাদনা করুন</string>
<string name="title_follow_requests">অনুরোধ অনুসরণ করুন</string> <string name="title_follow_requests">অনুরোধ অনুসরণ করুন</string>
<string name="title_blocks">অবরুদ্ধ ব্যবহারকারী</string> <string name="title_blocks">অবরুদ্ধ ব্যবহারকারী</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">অবরুদ্ধ ব্যবহারকারী</string> <string name="title_blocks">অবরুদ্ধ ব্যবহারকারী</string>
<string name="title_follow_requests">অনুরোধ অনুসরণ করুন</string> <string name="title_follow_requests">অনুরোধ অনুসরণ করুন</string>
<string name="title_edit_profile">আপনার প্রোফাইল সম্পাদনা করুন</string> <string name="title_edit_profile">আপনার প্রোফাইল সম্পাদনা করুন</string>
<string name="title_saved_toot">খসড়াগুলো</string> <string name="title_drafts">খসড়াগুলো</string>
<string name="title_licenses">লাইসেন্সগুলি</string> <string name="title_licenses">লাইসেন্সগুলি</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s সমর্থন দিয়েছে</string> <string name="status_boosted_format">%s সমর্থন দিয়েছে</string>

View file

@ -29,7 +29,7 @@
<string name="title_blocks">Usuaris blocats</string> <string name="title_blocks">Usuaris blocats</string>
<string name="title_follow_requests">Peticions de seguiment</string> <string name="title_follow_requests">Peticions de seguiment</string>
<string name="title_edit_profile">Edita el perfil</string> <string name="title_edit_profile">Edita el perfil</string>
<string name="title_saved_toot">Esborranys</string> <string name="title_drafts">Esborranys</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s tootejat</string> <string name="status_boosted_format">%s tootejat</string>
<string name="status_sensitive_media_title">Contingut sensible</string> <string name="status_sensitive_media_title">Contingut sensible</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">Blokovaní uživatelé</string> <string name="title_blocks">Blokovaní uživatelé</string>
<string name="title_follow_requests">Žádosti o sledování</string> <string name="title_follow_requests">Žádosti o sledování</string>
<string name="title_edit_profile">Upravit váš profil</string> <string name="title_edit_profile">Upravit váš profil</string>
<string name="title_saved_toot">Koncepty</string> <string name="title_drafts">Koncepty</string>
<string name="title_licenses">Licence</string> <string name="title_licenses">Licence</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s boostnul/a</string> <string name="status_boosted_format">%s boostnul/a</string>

View file

@ -32,7 +32,7 @@
<string name="title_blocks">Defnyddwyr wedi\'u blocio</string> <string name="title_blocks">Defnyddwyr wedi\'u blocio</string>
<string name="title_follow_requests">Dilyn ceisiadau</string> <string name="title_follow_requests">Dilyn ceisiadau</string>
<string name="title_edit_profile">Golygu\'ch Proffil</string> <string name="title_edit_profile">Golygu\'ch Proffil</string>
<string name="title_saved_toot">Drafftiau</string> <string name="title_drafts">Drafftiau</string>
<string name="title_licenses">Trwyddedau</string> <string name="title_licenses">Trwyddedau</string>
<string name="status_boosted_format">%s wedi\'u hybu</string> <string name="status_boosted_format">%s wedi\'u hybu</string>
<string name="status_sensitive_media_title">Cynnwys sensitif</string> <string name="status_sensitive_media_title">Cynnwys sensitif</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">Blockierte Profile</string> <string name="title_blocks">Blockierte Profile</string>
<string name="title_follow_requests">Folgeanfragen</string> <string name="title_follow_requests">Folgeanfragen</string>
<string name="title_edit_profile">Dein Profil bearbeiten</string> <string name="title_edit_profile">Dein Profil bearbeiten</string>
<string name="title_saved_toot">Entwürfe</string> <string name="title_drafts">Entwürfe</string>
<string name="title_licenses">Lizenzen</string> <string name="title_licenses">Lizenzen</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s teilte</string> <string name="status_boosted_format">%s teilte</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">Blokitaj uzantoj</string> <string name="title_blocks">Blokitaj uzantoj</string>
<string name="title_follow_requests">Petoj de sekvado</string> <string name="title_follow_requests">Petoj de sekvado</string>
<string name="title_edit_profile">Redakti vian profilon</string> <string name="title_edit_profile">Redakti vian profilon</string>
<string name="title_saved_toot">Malnetoj</string> <string name="title_drafts">Malnetoj</string>
<string name="title_licenses">Permesiloj</string> <string name="title_licenses">Permesiloj</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s diskonigis</string> <string name="status_boosted_format">%s diskonigis</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">Bloqueados</string> <string name="title_blocks">Bloqueados</string>
<string name="title_follow_requests">Solicitudes</string> <string name="title_follow_requests">Solicitudes</string>
<string name="title_edit_profile">Editar tu perfil</string> <string name="title_edit_profile">Editar tu perfil</string>
<string name="title_saved_toot">Borradores</string> <string name="title_drafts">Borradores</string>
<string name="title_licenses">Licencias</string> <string name="title_licenses">Licencias</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s compartió</string> <string name="status_boosted_format">%s compartió</string>

View file

@ -32,7 +32,7 @@
<string name="title_blocks">Blokeatuak</string> <string name="title_blocks">Blokeatuak</string>
<string name="title_follow_requests">Eskakizunak</string> <string name="title_follow_requests">Eskakizunak</string>
<string name="title_edit_profile">Profila editatu</string> <string name="title_edit_profile">Profila editatu</string>
<string name="title_saved_toot">Zirriborroak</string> <string name="title_drafts">Zirriborroak</string>
<string name="title_licenses">Lizentziak</string> <string name="title_licenses">Lizentziak</string>
<string name="status_boosted_format">%s-(e)k bultzatu du</string> <string name="status_boosted_format">%s-(e)k bultzatu du</string>
<string name="status_sensitive_media_title">Kontuz edukiarekin</string> <string name="status_sensitive_media_title">Kontuz edukiarekin</string>

View file

@ -32,7 +32,7 @@
<string name="title_blocks">کاربران مسدود</string> <string name="title_blocks">کاربران مسدود</string>
<string name="title_follow_requests">درخواست‌های پی‌گیری</string> <string name="title_follow_requests">درخواست‌های پی‌گیری</string>
<string name="title_edit_profile">ویرایش نمایه‌تان</string> <string name="title_edit_profile">ویرایش نمایه‌تان</string>
<string name="title_saved_toot">پیش‌نویس‌ها</string> <string name="title_drafts">پیش‌نویس‌ها</string>
<string name="title_licenses">پروانه‌ها</string> <string name="title_licenses">پروانه‌ها</string>
<string name="status_boosted_format">%s تقویت کرد</string> <string name="status_boosted_format">%s تقویت کرد</string>
<string name="status_sensitive_media_title">محتوای حسّاس</string> <string name="status_sensitive_media_title">محتوای حسّاس</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">Comptes bloqués</string> <string name="title_blocks">Comptes bloqués</string>
<string name="title_follow_requests">Demandes dabonnement</string> <string name="title_follow_requests">Demandes dabonnement</string>
<string name="title_edit_profile">Modifier votre profil</string> <string name="title_edit_profile">Modifier votre profil</string>
<string name="title_saved_toot">Brouillons</string> <string name="title_drafts">Brouillons</string>
<string name="title_licenses">Licences</string> <string name="title_licenses">Licences</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s a partagé</string> <string name="status_boosted_format">%s a partagé</string>

View file

@ -177,7 +177,7 @@
<string name="action_view_account_preferences">Roghanna Cuntais</string> <string name="action_view_account_preferences">Roghanna Cuntais</string>
<string name="action_view_preferences">Sainroghanna</string> <string name="action_view_preferences">Sainroghanna</string>
<string name="action_logout">Logáil Amach</string> <string name="action_logout">Logáil Amach</string>
<string name="title_saved_toot">Dréachtaí</string> <string name="title_drafts">Dréachtaí</string>
<string name="title_favourites">Roghaí</string> <string name="title_favourites">Roghaí</string>
<string name="error_failed_app_registration">Theip ar fhíordheimhniú leis an gcás sin.</string> <string name="error_failed_app_registration">Theip ar fhíordheimhniú leis an gcás sin.</string>
<string name="link_whats_an_instance">Cad is sampla ann\?</string> <string name="link_whats_an_instance">Cad is sampla ann\?</string>

View file

@ -8,7 +8,7 @@
<string name="action_view_account_preferences">Roighainnean cunntais</string> <string name="action_view_account_preferences">Roighainnean cunntais</string>
<string name="action_view_preferences">Roighainnean</string> <string name="action_view_preferences">Roighainnean</string>
<string name="action_logout">Clàraich a-mach</string> <string name="action_logout">Clàraich a-mach</string>
<string name="title_saved_toot">Dreachd</string> <string name="title_drafts">Dreachd</string>
<string name="title_favourites">Prìomhaich</string> <string name="title_favourites">Prìomhaich</string>
<string name="link_whats_an_instance">Dè a th ann an àite\?</string> <string name="link_whats_an_instance">Dè a th ann an àite\?</string>
<string name="edit_poll">Deasaich</string> <string name="edit_poll">Deasaich</string>

View file

@ -2,7 +2,7 @@
<resources> <resources>
<string name="action_login">हिंदी</string> <string name="action_login">हिंदी</string>
<string name="title_favourites">पसंदीदा</string> <string name="title_favourites">पसंदीदा</string>
<string name="title_saved_toot">प्रारूप</string> <string name="title_drafts">प्रारूप</string>
<string name="action_logout">लॉग आउट</string> <string name="action_logout">लॉग आउट</string>
<string name="action_view_preferences">पसंद</string> <string name="action_view_preferences">पसंद</string>
<string name="action_view_account_preferences">खाता प्राथमिकताएं</string> <string name="action_view_account_preferences">खाता प्राथमिकताएं</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">Letiltott felhasználók</string> <string name="title_blocks">Letiltott felhasználók</string>
<string name="title_follow_requests">Követési kérelmek</string> <string name="title_follow_requests">Követési kérelmek</string>
<string name="title_edit_profile">Profilod szerkesztése</string> <string name="title_edit_profile">Profilod szerkesztése</string>
<string name="title_saved_toot">Piszkozatok</string> <string name="title_drafts">Piszkozatok</string>
<string name="title_licenses">Licenszek</string> <string name="title_licenses">Licenszek</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s megtolta</string> <string name="status_boosted_format">%s megtolta</string>

View file

@ -3,7 +3,7 @@
<string name="action_login">Skrá inn með Mastodon</string> <string name="action_login">Skrá inn með Mastodon</string>
<string name="link_whats_an_instance">Hvað er tilvik\?</string> <string name="link_whats_an_instance">Hvað er tilvik\?</string>
<string name="title_favourites">Eftirlæti</string> <string name="title_favourites">Eftirlæti</string>
<string name="title_saved_toot">Drög</string> <string name="title_drafts">Drög</string>
<string name="action_logout">Skrá út</string> <string name="action_logout">Skrá út</string>
<string name="action_view_preferences">Kjörstillingar</string> <string name="action_view_preferences">Kjörstillingar</string>
<string name="action_view_account_preferences">Eiginleikar tengingar</string> <string name="action_view_account_preferences">Eiginleikar tengingar</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">Utenti bloccati</string> <string name="title_blocks">Utenti bloccati</string>
<string name="title_follow_requests">Richieste di seguirti</string> <string name="title_follow_requests">Richieste di seguirti</string>
<string name="title_edit_profile">Modifica il tuo profilo</string> <string name="title_edit_profile">Modifica il tuo profilo</string>
<string name="title_saved_toot">Bozze</string> <string name="title_drafts">Bozze</string>
<string name="title_licenses">Licenze</string> <string name="title_licenses">Licenze</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s ha boostato</string> <string name="status_boosted_format">%s ha boostato</string>

View file

@ -35,7 +35,7 @@
<string name="title_blocks">ブロックしたユーザー</string> <string name="title_blocks">ブロックしたユーザー</string>
<string name="title_follow_requests">フォローリクエスト</string> <string name="title_follow_requests">フォローリクエスト</string>
<string name="title_edit_profile">プロフィールを編集</string> <string name="title_edit_profile">プロフィールを編集</string>
<string name="title_saved_toot">下書き</string> <string name="title_drafts">下書き</string>
<string name="title_licenses">ライセンス</string> <string name="title_licenses">ライセンス</string>
<string name="status_boosted_format">%sさんがブーストしました</string> <string name="status_boosted_format">%sさんがブーストしました</string>
<string name="status_sensitive_media_title">閲覧注意</string> <string name="status_sensitive_media_title">閲覧注意</string>

View file

@ -2,7 +2,7 @@
<resources> <resources>
<string name="action_login">Qqen ɣer Maṣṭudun</string> <string name="action_login">Qqen ɣer Maṣṭudun</string>
<string name="title_favourites">Ismenyifen</string> <string name="title_favourites">Ismenyifen</string>
<string name="title_saved_toot">Irewwayen</string> <string name="title_drafts">Irewwayen</string>
<string name="action_logout">Ffeɣ</string> <string name="action_logout">Ffeɣ</string>
<string name="action_view_preferences">Iɣewwaṛen</string> <string name="action_view_preferences">Iɣewwaṛen</string>
<string name="action_view_account_preferences">Iɣewwaṛen n umiḍan</string> <string name="action_view_account_preferences">Iɣewwaṛen n umiḍan</string>

View file

@ -37,7 +37,7 @@
<string name="title_domain_mutes">숨긴 도메인</string> <string name="title_domain_mutes">숨긴 도메인</string>
<string name="title_follow_requests">팔로우 요청</string> <string name="title_follow_requests">팔로우 요청</string>
<string name="title_edit_profile">프로필 편집</string> <string name="title_edit_profile">프로필 편집</string>
<string name="title_saved_toot">임시 저장</string> <string name="title_drafts">임시 저장</string>
<string name="title_licenses">라이선스</string> <string name="title_licenses">라이선스</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s님이 부스트 했습니다</string> <string name="status_boosted_format">%s님이 부스트 했습니다</string>

View file

@ -3,7 +3,7 @@
<string name="action_login">മസ്റ്റഡോൺ വഴി പ്രവേശിക്കുക</string> <string name="action_login">മസ്റ്റഡോൺ വഴി പ്രവേശിക്കുക</string>
<string name="link_whats_an_instance">എന്താണ് ഒരു ഇൻസ്റ്റൻസ്\?</string> <string name="link_whats_an_instance">എന്താണ് ഒരു ഇൻസ്റ്റൻസ്\?</string>
<string name="title_favourites">പ്രിയപ്പെട്ടവ</string> <string name="title_favourites">പ്രിയപ്പെട്ടവ</string>
<string name="title_saved_toot">കരടുകൾ</string> <string name="title_drafts">കരടുകൾ</string>
<string name="action_logout">പുറത്തിറങ്ങുക</string> <string name="action_logout">പുറത്തിറങ്ങുക</string>
<string name="action_view_preferences">മുൻഗണനകൾ</string> <string name="action_view_preferences">മുൻഗണനകൾ</string>
<string name="action_view_account_preferences">അക്കൗണ്ട് മുൻഗണനകൾ</string> <string name="action_view_account_preferences">അക്കൗണ്ട് മുൻഗണനകൾ</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">Geblokkeerde gebruikers</string> <string name="title_blocks">Geblokkeerde gebruikers</string>
<string name="title_follow_requests">Volgverzoeken</string> <string name="title_follow_requests">Volgverzoeken</string>
<string name="title_edit_profile">Profiel bewerken</string> <string name="title_edit_profile">Profiel bewerken</string>
<string name="title_saved_toot">Concepten</string> <string name="title_drafts">Concepten</string>
<string name="title_licenses">Licenties</string> <string name="title_licenses">Licenties</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s boostte</string> <string name="status_boosted_format">%s boostte</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">Blokkerte brukere</string> <string name="title_blocks">Blokkerte brukere</string>
<string name="title_follow_requests">Forespørsler om følgen</string> <string name="title_follow_requests">Forespørsler om følgen</string>
<string name="title_edit_profile">Endre profilen din</string> <string name="title_edit_profile">Endre profilen din</string>
<string name="title_saved_toot">Kladder</string> <string name="title_drafts">Kladder</string>
<string name="title_licenses">Lisenser</string> <string name="title_licenses">Lisenser</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s boosted</string> <string name="status_boosted_format">%s boosted</string>

View file

@ -31,7 +31,7 @@
<string name="title_blocks">Utilizaires blocats</string> <string name="title_blocks">Utilizaires blocats</string>
<string name="title_follow_requests">Demandas dabonament</string> <string name="title_follow_requests">Demandas dabonament</string>
<string name="title_edit_profile">Modificar lo perfil</string> <string name="title_edit_profile">Modificar lo perfil</string>
<string name="title_saved_toot">Borrolhons</string> <string name="title_drafts">Borrolhons</string>
<string name="title_licenses">Licéncias</string> <string name="title_licenses">Licéncias</string>
<string name="status_boosted_format">%s partejat</string> <string name="status_boosted_format">%s partejat</string>
<string name="status_sensitive_media_title">Contengut sensible</string> <string name="status_sensitive_media_title">Contengut sensible</string>

View file

@ -31,7 +31,7 @@
<string name="title_blocks">Zablokowani użytkownicy</string> <string name="title_blocks">Zablokowani użytkownicy</string>
<string name="title_follow_requests">Prośby o możliwość śledzenia</string> <string name="title_follow_requests">Prośby o możliwość śledzenia</string>
<string name="title_edit_profile">Edytuj profil</string> <string name="title_edit_profile">Edytuj profil</string>
<string name="title_saved_toot">Szkice</string> <string name="title_drafts">Szkice</string>
<string name="title_licenses">Licencje</string> <string name="title_licenses">Licencje</string>
<string name="status_boosted_format">%s podbił</string> <string name="status_boosted_format">%s podbił</string>
<string name="status_sensitive_media_title">Wrażliwe treści</string> <string name="status_sensitive_media_title">Wrażliwe treści</string>

View file

@ -34,7 +34,7 @@
<string name="title_blocks">Usuários bloqueados</string> <string name="title_blocks">Usuários bloqueados</string>
<string name="title_follow_requests">Seguidores pendentes</string> <string name="title_follow_requests">Seguidores pendentes</string>
<string name="title_edit_profile">Editar perfil</string> <string name="title_edit_profile">Editar perfil</string>
<string name="title_saved_toot">Rascunhos</string> <string name="title_drafts">Rascunhos</string>
<string name="title_licenses">Licenças</string> <string name="title_licenses">Licenças</string>
<string name="status_boosted_format">%s deu boost</string> <string name="status_boosted_format">%s deu boost</string>
<string name="status_sensitive_media_title">Mídia sensível</string> <string name="status_sensitive_media_title">Mídia sensível</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">Список блокировки</string> <string name="title_blocks">Список блокировки</string>
<string name="title_follow_requests">Запросы на подписку</string> <string name="title_follow_requests">Запросы на подписку</string>
<string name="title_edit_profile">Редактировать профиль</string> <string name="title_edit_profile">Редактировать профиль</string>
<string name="title_saved_toot">Черновики</string> <string name="title_drafts">Черновики</string>
<string name="title_licenses">Лицензии</string> <string name="title_licenses">Лицензии</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s продвинул(а)</string> <string name="status_boosted_format">%s продвинул(а)</string>

View file

@ -36,7 +36,7 @@
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="title_licenses">अनुज्ञापत्राणि</string> <string name="title_licenses">अनुज्ञापत्राणि</string>
<string name="title_scheduled_toot">कालबद्धदौत्यानि</string> <string name="title_scheduled_toot">कालबद्धदौत्यानि</string>
<string name="title_saved_toot">लेखविकर्षाः</string> <string name="title_drafts">लेखविकर्षाः</string>
<string name="title_edit_profile">स्वीयव्यक्तिविवरणं सम्पाद्यताम्</string> <string name="title_edit_profile">स्वीयव्यक्तिविवरणं सम्पाद्यताम्</string>
<string name="title_follow_requests">अनुसरणार्थमनुरोधाः</string> <string name="title_follow_requests">अनुसरणार्थमनुरोधाः</string>
<string name="title_domain_mutes">प्रच्छन्नप्रदेशाः</string> <string name="title_domain_mutes">प्रच्छन्नप्रदेशाः</string>

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="action_login">Prihlásiť sa účtom Mastodon</string> <string name="action_login">Prihlásiť sa účtom Mastodon</string>
<string name="title_saved_toot">Koncepty</string> <string name="title_drafts">Koncepty</string>
<string name="action_logout">Odhlásiť sa</string> <string name="action_logout">Odhlásiť sa</string>
<string name="action_view_preferences">Nastavenia</string> <string name="action_view_preferences">Nastavenia</string>
<string name="action_view_account_preferences">Nastavenia účtu</string> <string name="action_view_account_preferences">Nastavenia účtu</string>

View file

@ -34,7 +34,7 @@
<string name="title_blocks">Blokirani uporabniki</string> <string name="title_blocks">Blokirani uporabniki</string>
<string name="title_follow_requests">Zahteve za Sledenje</string> <string name="title_follow_requests">Zahteve za Sledenje</string>
<string name="title_edit_profile">Uredi svoj profil</string> <string name="title_edit_profile">Uredi svoj profil</string>
<string name="title_saved_toot">Osnutki</string> <string name="title_drafts">Osnutki</string>
<string name="title_licenses">Licence</string> <string name="title_licenses">Licence</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_sensitive_media_title">Občutljiva vsebina</string> <string name="status_sensitive_media_title">Občutljiva vsebina</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">Blockerade användare</string> <string name="title_blocks">Blockerade användare</string>
<string name="title_follow_requests">Följarförfrågningar</string> <string name="title_follow_requests">Följarförfrågningar</string>
<string name="title_edit_profile">Ändra din profil</string> <string name="title_edit_profile">Ändra din profil</string>
<string name="title_saved_toot">Utkast</string> <string name="title_drafts">Utkast</string>
<string name="title_licenses">Licenser</string> <string name="title_licenses">Licenser</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s knuffade</string> <string name="status_boosted_format">%s knuffade</string>

View file

@ -29,7 +29,7 @@
<string name="title_blocks">தடைசெய்யபட்ட பயனர்கள்</string> <string name="title_blocks">தடைசெய்யபட்ட பயனர்கள்</string>
<string name="title_follow_requests">பின்பற்ற கோரிக்கை</string> <string name="title_follow_requests">பின்பற்ற கோரிக்கை</string>
<string name="title_edit_profile">சுயவிவரத்தை திருத்த</string> <string name="title_edit_profile">சுயவிவரத்தை திருத்த</string>
<string name="title_saved_toot">வரைவுகள்</string> <string name="title_drafts">வரைவுகள்</string>
<string name="status_boosted_format">%s மேலேற்றப்பட்டது</string> <string name="status_boosted_format">%s மேலேற்றப்பட்டது</string>
<string name="status_sensitive_media_title">உணர்ச்சிகரமான உள்ளடக்கம்</string> <string name="status_sensitive_media_title">உணர்ச்சிகரமான உள்ளடக்கம்</string>
<string name="status_media_hidden_title">ஊடகம் மறைக்கப்பட்டது</string> <string name="status_media_hidden_title">ஊடகம் மறைக்கப்பட்டது</string>

View file

@ -431,7 +431,7 @@
<string name="action_view_account_preferences">ตั้งค่าบัญชี</string> <string name="action_view_account_preferences">ตั้งค่าบัญชี</string>
<string name="action_view_preferences">ตั้งค่า</string> <string name="action_view_preferences">ตั้งค่า</string>
<string name="action_logout">ออกจากระบบ</string> <string name="action_logout">ออกจากระบบ</string>
<string name="title_saved_toot">ฉบับร่าง</string> <string name="title_drafts">ฉบับร่าง</string>
<string name="title_favourites">ชื่นชอบ</string> <string name="title_favourites">ชื่นชอบ</string>
<string name="error_failed_app_registration">การยืนยันตัวตนกับเซิร์ฟเวอร์นั้นล้มเหลว</string> <string name="error_failed_app_registration">การยืนยันตัวตนกับเซิร์ฟเวอร์นั้นล้มเหลว</string>
<string name="link_whats_an_instance">Instance คือ\?</string> <string name="link_whats_an_instance">Instance คือ\?</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">Engellenmiş kullanıcılar</string> <string name="title_blocks">Engellenmiş kullanıcılar</string>
<string name="title_follow_requests">Takip Etme İstekleri</string> <string name="title_follow_requests">Takip Etme İstekleri</string>
<string name="title_edit_profile">Profili düzeltme</string> <string name="title_edit_profile">Profili düzeltme</string>
<string name="title_saved_toot">Taslaklar</string> <string name="title_drafts">Taslaklar</string>
<string name="title_licenses">Lisanslar</string> <string name="title_licenses">Lisanslar</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s yineledi</string> <string name="status_boosted_format">%s yineledi</string>

View file

@ -37,7 +37,7 @@
<string name="action_view_account_preferences">Налаштування акаунта</string> <string name="action_view_account_preferences">Налаштування акаунта</string>
<string name="action_view_preferences">Налаштування</string> <string name="action_view_preferences">Налаштування</string>
<string name="action_logout">Вийти</string> <string name="action_logout">Вийти</string>
<string name="title_saved_toot">Чернетки</string> <string name="title_drafts">Чернетки</string>
<string name="title_favourites">Вподобане</string> <string name="title_favourites">Вподобане</string>
<string name="action_login">Увійти</string> <string name="action_login">Увійти</string>
<string name="login_connection">Зʼєднання…</string> <string name="login_connection">Зʼєднання…</string>

View file

@ -197,7 +197,7 @@
<string name="title_public_local">Cộng đồng</string> <string name="title_public_local">Cộng đồng</string>
<string name="title_notifications">Thông báo</string> <string name="title_notifications">Thông báo</string>
<string name="title_home">Bảng tin</string> <string name="title_home">Bảng tin</string>
<string name="title_saved_toot">Nháp</string> <string name="title_drafts">Nháp</string>
<string name="title_favourites">Lượt thích</string> <string name="title_favourites">Lượt thích</string>
<string name="link_whats_an_instance">Máy chủ là gì\?</string> <string name="link_whats_an_instance">Máy chủ là gì\?</string>
<string name="pref_title_show_media_preview">Tải xem trước hình ảnh</string> <string name="pref_title_show_media_preview">Tải xem trước hình ảnh</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">被屏蔽的用户</string> <string name="title_blocks">被屏蔽的用户</string>
<string name="title_follow_requests">关注请求</string> <string name="title_follow_requests">关注请求</string>
<string name="title_edit_profile">编辑个人资料</string> <string name="title_edit_profile">编辑个人资料</string>
<string name="title_saved_toot">草稿</string> <string name="title_drafts">草稿</string>
<string name="title_licenses">开源协议</string> <string name="title_licenses">开源协议</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s 转嘟了</string> <string name="status_boosted_format">%s 转嘟了</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">被封鎖的使用者</string> <string name="title_blocks">被封鎖的使用者</string>
<string name="title_follow_requests">關注請求</string> <string name="title_follow_requests">關注請求</string>
<string name="title_edit_profile">編輯個人資料</string> <string name="title_edit_profile">編輯個人資料</string>
<string name="title_saved_toot">草稿</string> <string name="title_drafts">草稿</string>
<string name="title_licenses">開源授權</string> <string name="title_licenses">開源授權</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s 轉嘟了</string> <string name="status_boosted_format">%s 轉嘟了</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">被封鎖的使用者</string> <string name="title_blocks">被封鎖的使用者</string>
<string name="title_follow_requests">關注請求</string> <string name="title_follow_requests">關注請求</string>
<string name="title_edit_profile">編輯個人資料</string> <string name="title_edit_profile">編輯個人資料</string>
<string name="title_saved_toot">草稿</string> <string name="title_drafts">草稿</string>
<string name="title_licenses">開源授權</string> <string name="title_licenses">開源授權</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s 轉嘟了</string> <string name="status_boosted_format">%s 轉嘟了</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">被屏蔽的用户</string> <string name="title_blocks">被屏蔽的用户</string>
<string name="title_follow_requests">关注请求</string> <string name="title_follow_requests">关注请求</string>
<string name="title_edit_profile">编辑个人资料</string> <string name="title_edit_profile">编辑个人资料</string>
<string name="title_saved_toot">草稿</string> <string name="title_drafts">草稿</string>
<string name="title_licenses">开源协议</string> <string name="title_licenses">开源协议</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s 转嘟了</string> <string name="status_boosted_format">%s 转嘟了</string>

View file

@ -36,7 +36,7 @@
<string name="title_blocks">被封鎖的使用者</string> <string name="title_blocks">被封鎖的使用者</string>
<string name="title_follow_requests">關注請求</string> <string name="title_follow_requests">關注請求</string>
<string name="title_edit_profile">編輯個人資料</string> <string name="title_edit_profile">編輯個人資料</string>
<string name="title_saved_toot">草稿</string> <string name="title_drafts">草稿</string>
<string name="title_licenses">開源授權</string> <string name="title_licenses">開源授權</string>
<string name="status_username_format">\@%s</string> <string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s 轉嘟了</string> <string name="status_boosted_format">%s 轉嘟了</string>

View file

@ -45,6 +45,7 @@
<dimen name="card_radius">5dp</dimen> <dimen name="card_radius">5dp</dimen>
<dimen name="poll_preview_padding">12dp</dimen> <dimen name="poll_preview_padding">12dp</dimen>
<dimen name="poll_preview_min_width">120dp</dimen>
<dimen name="adaptive_bitmap_inner_size">72dp</dimen> <dimen name="adaptive_bitmap_inner_size">72dp</dimen>
<dimen name="adaptive_bitmap_outer_size">108dp</dimen> <dimen name="adaptive_bitmap_outer_size">108dp</dimen>

View file

@ -40,7 +40,7 @@
<string name="title_domain_mutes">Hidden domains</string> <string name="title_domain_mutes">Hidden domains</string>
<string name="title_follow_requests">Follow Requests</string> <string name="title_follow_requests">Follow Requests</string>
<string name="title_edit_profile">Edit your profile</string> <string name="title_edit_profile">Edit your profile</string>
<string name="title_saved_toot">Drafts</string> <string name="title_drafts">Drafts</string>
<string name="title_scheduled_toot">Scheduled toots</string> <string name="title_scheduled_toot">Scheduled toots</string>
<string name="title_announcements">Announcements</string> <string name="title_announcements">Announcements</string>
<string name="title_licenses">Licenses</string> <string name="title_licenses">Licenses</string>
@ -585,6 +585,7 @@
<string name="pref_title_wellbeing_mode">Wellbeing</string> <string name="pref_title_wellbeing_mode">Wellbeing</string>
<string name="account_note_hint">Your private note about this account</string> <string name="account_note_hint">Your private note about this account</string>
<string name="account_note_saved">Saved!</string> <string name="account_note_saved">Saved!</string>
<string name="wellbeing_mode_notice">Some information that might affect your mental wellbeing will be hidden. This includes:\n\n <string name="wellbeing_mode_notice">Some information that might affect your mental wellbeing will be hidden. This includes:\n\n
- Favorite/Boost/Follow notifications\n - Favorite/Boost/Follow notifications\n
- Favorite/Boost count on toots\n - Favorite/Boost count on toots\n
@ -598,4 +599,15 @@
<string name="error_upload_max_media_reached">You cannot upload more than %1$d media attachments.</string> <string name="error_upload_max_media_reached">You cannot upload more than %1$d media attachments.</string>
<string name="dialog_delete_list_warning">Do you really want to delete the list %s?</string> <string name="dialog_delete_list_warning">Do you really want to delete the list %s?</string>
<string name="drafts_toot_failed_to_send">This toot failed to send!</string>
<string name="new_drafts_warning">
The draft feature in Tusky has been completely redesigned to be faster, more user friendly and less buggy.\n
You can still access your old drafts via a button on the new drafts screen,
but they will be removed in a future update!
</string>
<string name="old_drafts">Old Drafts</string>
<string name="drafts_failed_loading_reply">Failed loading Reply information</string>
<string name="draft_deleted">Draft deleted</string>
<string name="drafts_toot_reply_removed">The Toot you drafted a reply to has been removed</string>
</resources> </resources>

View file

@ -13,7 +13,6 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not, * You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */ * see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky package com.keylesspalace.tusky
import android.content.Intent import android.content.Intent
@ -25,6 +24,7 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.compose.ComposeViewModel
import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT
import com.keylesspalace.tusky.components.compose.MediaUploader import com.keylesspalace.tusky.components.compose.MediaUploader
import com.keylesspalace.tusky.components.drafts.DraftHelper
import com.keylesspalace.tusky.db.* import com.keylesspalace.tusky.db.*
import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Account
@ -115,6 +115,7 @@ class ComposeActivityTest {
accountManagerMock, accountManagerMock,
mock(MediaUploader::class.java), mock(MediaUploader::class.java),
mock(ServiceClient::class.java), mock(ServiceClient::class.java),
mock(DraftHelper::class.java),
mock(SaveTootHelper::class.java), mock(SaveTootHelper::class.java),
dbMock dbMock
) )