diff --git a/app/build.gradle b/app/build.gradle
index a7f566ac6..fa6fc401d 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -67,6 +67,9 @@ android {
androidExtensions {
experimental = true
}
+ buildFeatures {
+ viewBinding true
+ }
testOptions {
unitTests {
returnDefaultValues = true
diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json
new file mode 100644
index 000000000..01a491b4a
--- /dev/null
+++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json
@@ -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')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 585ff833c..770d45af6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -146,6 +146,7 @@
+
+ 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 androidInjector() = androidInjector
diff --git a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java
index 9a1639898..8e2b5acb2 100644
--- a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java
@@ -89,7 +89,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
setSupportActionBar(toolbar);
ActionBar bar = getSupportActionBar();
if (bar != null) {
- bar.setTitle(getString(R.string.title_saved_toot));
+ bar.setTitle(getString(R.string.title_drafts));
bar.setDisplayHomeAsUpEnabled(true);
bar.setDisplayShowHomeEnabled(true);
}
@@ -166,6 +166,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
ComposeOptions composeOptions = new ComposeOptions(
/*scheduledTootUid*/null,
item.getUid(),
+ /*drafId*/null,
item.getText(),
jsonUrls,
descriptions,
@@ -177,6 +178,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
item.getInReplyToUsername(),
item.getInReplyToText(),
/*mediaAttachments*/null,
+ /*draftAttachments*/null,
/*scheduledAt*/null,
/*sensitive*/null,
/*poll*/null,
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
index 5306e57ae..69a090880 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt
@@ -30,7 +30,6 @@ import android.os.Build
import android.os.Bundle
import android.os.Parcelable
import android.provider.MediaStore
-import android.text.TextUtils
import android.util.Log
import android.view.KeyEvent
import android.view.MenuItem
@@ -57,13 +56,13 @@ import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.BaseActivity
import com.keylesspalace.tusky.BuildConfig
import com.keylesspalace.tusky.R
-import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter
import com.keylesspalace.tusky.adapter.EmojiAdapter
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener
import com.keylesspalace.tusky.db.AccountEntity
+import com.keylesspalace.tusky.db.DraftAttachment
import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Attachment
@@ -81,7 +80,6 @@ import java.io.File
import java.io.IOException
import java.util.*
import javax.inject.Inject
-import kotlin.collections.ArrayList
import kotlin.math.max
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
private var finishingUploadDialog: ProgressDialog? = null
private var photoUploadUri: Uri? = null
+
@VisibleForTesting
var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT
- private var composeOptions: ComposeOptions? = null
private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
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
* based on what the intent from the reply request passes. */
- if (intent != null) {
- this.composeOptions = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA)
- viewModel.setup(composeOptions)
- setupReplyViews(composeOptions?.replyingStatusAuthor)
- val tootText = composeOptions?.tootText
- if (!tootText.isNullOrEmpty()) {
- composeEditField.setText(tootText)
- }
+
+ val composeOptions: ComposeOptions? = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA)
+
+ viewModel.setup(composeOptions)
+ setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent)
+ val tootText = composeOptions?.tootText
+ if (!tootText.isNullOrEmpty()) {
+ composeEditField.setText(tootText)
}
- if (!TextUtils.isEmpty(composeOptions?.scheduledAt)) {
+ if (!composeOptions?.scheduledAt.isNullOrEmpty()) {
composeScheduleView.setDateTime(composeOptions?.scheduledAt)
}
@@ -169,38 +167,24 @@ class ComposeActivity : BaseActivity(),
viewModel.setupComplete.value = true
}
- private fun applyShareIntent(intent: Intent?, savedInstanceState: Bundle?) {
- if (intent != null && savedInstanceState == null) {
+ private fun applyShareIntent(intent: Intent, savedInstanceState: Bundle?) {
+ if (savedInstanceState == null) {
/* 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
* instance state will be re-queued. */
- val type = intent.type
- if (type != null) {
+ intent.type?.also { type ->
if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) {
- val uriList = ArrayList()
- if (intent.action != null) {
- when (intent.action) {
- Intent.ACTION_SEND -> {
- val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM)
- if (uri != null) {
- uriList.add(uri)
- }
- }
- Intent.ACTION_SEND_MULTIPLE -> {
- val list = intent.getParcelableArrayListExtra(
- Intent.EXTRA_STREAM)
- if (list != null) {
- for (uri in list) {
- if (uri != null) {
- uriList.add(uri)
- }
- }
- }
+ when (intent.action) {
+ Intent.ACTION_SEND -> {
+ intent.getParcelableExtra(Intent.EXTRA_STREAM)?.let { uri ->
+ pickMedia(uri)
+ }
+ }
+ Intent.ACTION_SEND_MULTIPLE -> {
+ intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)?.forEach { uri ->
+ pickMedia(uri)
}
}
- }
- for (uri in uriList) {
- pickMedia(uri)
}
} 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) {
composeReplyView.show()
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?) {
@@ -651,7 +635,6 @@ class ComposeActivity : BaseActivity(),
}
}
-
private fun removePoll() {
viewModel.poll.value = null
pollPreview.hide()
@@ -835,22 +818,22 @@ class ComposeActivity : BaseActivity(),
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
super.onActivityResult(requestCode, resultCode, intent)
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.
pickMedia(intent.data!!)
- }else if(intent.clipData != null){
+ } else if (intent.clipData != null) {
val clipData = intent.clipData!!
val count = clipData.itemCount
- if(mediaCount + count > maxUploadMediaNumber){
+ if (mediaCount + count > maxUploadMediaNumber) {
// 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()
- }else{
+ } else {
// if not grater then 4, upload all multiple media.
for (i in 0 until count) {
- val imageUri = clipData.getItemAt(i).getUri()
- pickMedia(imageUri)
- }
+ val imageUri = clipData.getItemAt(i).getUri()
+ pickMedia(imageUri)
}
+ }
}
} else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) {
pickMedia(photoUploadUri!!)
@@ -1018,8 +1001,9 @@ class ComposeActivity : BaseActivity(),
@Parcelize
data class ComposeOptions(
// Let's keep fields var until all consumers are Kotlin
- var scheduledTootUid: String? = null,
+ var scheduledTootId: String? = null,
var savedTootUid: Int? = null,
+ var draftId: Int? = null,
var tootText: String? = null,
var mediaUrls: List? = null,
var mediaDescriptions: List? = null,
@@ -1031,6 +1015,7 @@ class ComposeActivity : BaseActivity(),
var replyingStatusAuthor: String? = null,
var replyingStatusContent: String? = null,
var mediaAttachments: List? = null,
+ var draftAttachments: List? = null,
var scheduledAt: String? = null,
var sensitive: Boolean? = null,
var poll: NewPoll? = null,
@@ -1057,7 +1042,6 @@ class ComposeActivity : BaseActivity(),
}
}
- @JvmStatic
fun canHandleMimeType(mimeType: String?): Boolean {
return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain")
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java
similarity index 99%
rename from app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java
rename to app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java
index ebc292ab4..df9ae8a81 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java
+++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java
@@ -13,7 +13,7 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see . */
-package com.keylesspalace.tusky.adapter;
+package com.keylesspalace.tusky.components.compose;
import android.content.Context;
import android.preference.PreferenceManager;
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt
index 2c015899b..a1a5e7e63 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt
@@ -21,8 +21,8 @@ import androidx.core.net.toUri
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
-import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter
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.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
@@ -39,18 +39,12 @@ import io.reactivex.rxkotlin.Singles
import java.util.*
import javax.inject.Inject
-/**
- * Throw when trying to add an image when video is already present or the other way around
- */
-class VideoOrImageException : Exception()
-
-
-class ComposeViewModel
-@Inject constructor(
+class ComposeViewModel @Inject constructor(
private val api: MastodonApi,
private val accountManager: AccountManager,
private val mediaUploader: MediaUploader,
private val serviceClient: ServiceClient,
+ private val draftHelper: DraftHelper,
private val saveTootHelper: SaveTootHelper,
private val db: AppDatabase
) : RxAwareViewModel() {
@@ -59,7 +53,8 @@ class ComposeViewModel
private var replyingStatusContent: String? = null
internal var startingText: String? = null
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 inReplyToId: String? = null
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
@@ -81,10 +76,6 @@ class ComposeViewModel
val markMediaAsSensitive =
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
- fun toggleMarkSensitive() {
- this.markMediaAsSensitive.value = !this.markMediaAsSensitive.value!!
- }
-
val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN)
val showContentWarning = mutableLiveData(false)
val setupComplete = mutableLiveData(false)
@@ -96,7 +87,7 @@ class ComposeViewModel
private val mediaToDisposable = mutableMapOf()
- private val isEditingScheduledToot get() = !scheduledTootUid.isNullOrEmpty()
+ private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty()
init {
@@ -116,7 +107,7 @@ class ComposeViewModel
.onErrorResumeNext(
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
)
- .subscribe ({ instanceEntity ->
+ .subscribe({ instanceEntity ->
emoji.postValue(instanceEntity.emojiList)
instance.postValue(instanceEntity)
}, { throwable ->
@@ -126,7 +117,7 @@ class ComposeViewModel
.autoDispose()
}
- fun pickMedia(uri: Uri): LiveData> {
+ fun pickMedia(uri: Uri, description: String? = null): LiveData> {
// We are not calling .toLiveData() here because we don't want to stop the process when
// the Activity goes away temporarily (like on screen rotation).
val liveData = MutableLiveData>()
@@ -138,7 +129,7 @@ class ComposeViewModel
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) {
throw VideoOrImageException()
} else {
- addMediaToQueue(type, uri, size)
+ addMediaToQueue(type, uri, size, description)
}
}
.subscribe({ queuedMedia ->
@@ -150,12 +141,23 @@ class ComposeViewModel
return liveData
}
- private fun addMediaToQueue(type: QueuedMedia.Type, uri: Uri, mediaSize: Long): QueuedMedia {
- val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, mediaSize)
+ private fun addMediaToQueue(
+ 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
mediaToDisposable[mediaItem.localId] = mediaUploader
.uploadMedia(mediaItem)
- .subscribe ({ event ->
+ .subscribe({ event ->
val item = media.value?.find { it.localId == mediaItem.localId }
?: return@subscribe
val newMediaItem = when (event) {
@@ -190,6 +192,10 @@ class ComposeViewModel
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 {
val textChanged = !(content.isNullOrEmpty()
@@ -210,29 +216,37 @@ class ComposeViewModel
}
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) {
- val mediaUris = mutableListOf()
- val mediaDescriptions = mutableListOf()
- for (item in media.value!!) {
+
+ val mediaUris: MutableList = mutableListOf()
+ val mediaDescriptions: MutableList = mutableListOf()
+ media.value?.forEach { item ->
mediaUris.add(item.uri.toString())
mediaDescriptions.add(item.description)
}
- saveTootHelper.saveToot(
- content,
- contentWarning,
- null,
- mediaUris,
- mediaDescriptions,
- savedTootUid,
- inReplyToId,
- replyingStatusContent,
- replyingStatusAuthor,
- statusVisibility.value!!,
- poll.value
- )
+
+ draftHelper.saveDraft(
+ draftId = draftId,
+ accountId = accountManager.activeAccount?.id!!,
+ inReplyToId = inReplyToId,
+ content = content,
+ contentWarning = contentWarning,
+ sensitive = markMediaAsSensitive.value!!,
+ visibility = statusVisibility.value!!,
+ mediaUris = mediaUris,
+ mediaDescriptions = mediaDescriptions,
+ poll = poll.value,
+ failedToSend = false
+ ).subscribe()
}
/**
@@ -246,7 +260,7 @@ class ComposeViewModel
): LiveData {
val deletionObservable = if (isEditingScheduledToot) {
- api.deleteScheduledStatus(scheduledTootUid.toString()).toObservable().map { Unit }
+ api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { }
} else {
just(Unit)
}.toLiveData()
@@ -257,28 +271,30 @@ class ComposeViewModel
val mediaIds = ArrayList()
val mediaUris = ArrayList()
val mediaDescriptions = ArrayList()
+ val mediaTypes = ArrayList()
for (item in media.value!!) {
mediaIds.add(item.id!!)
mediaUris.add(item.uri)
mediaDescriptions.add(item.description ?: "")
+ mediaTypes.add(item.type)
}
val tootToSend = TootToSend(
- content,
- spoilerText,
- statusVisibility.value!!.serverString(),
- mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
- mediaIds,
- mediaUris.map { it.toString() },
- mediaDescriptions,
+ text = content,
+ warningText = spoilerText,
+ visibility = statusVisibility.value!!.serverString(),
+ sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
+ mediaIds = mediaIds,
+ mediaUris = mediaUris.map { it.toString() },
+ mediaDescriptions = mediaDescriptions,
scheduledAt = scheduledAt.value,
inReplyToId = inReplyToId,
poll = poll.value,
replyingStatusContent = null,
replyingStatusAuthorUsername = null,
- savedJsonUrls = null,
accountId = accountManager.activeAccount!!.id,
- savedTootUid = 0,
+ savedTootUid = savedTootUid,
+ draftId = draftId,
idempotencyKey = randomAlphanumericString(16),
retries = 0
)
@@ -286,9 +302,7 @@ class ComposeViewModel
serviceClient.sendToot(tootToSend)
}
- return combineLiveData(deletionObservable, sendObservable) { _, _ -> Unit }
-
-
+ return combineLiveData(deletionObservable, sendObservable) { _, _ -> }
}
fun updateDescription(localId: Long, description: String): LiveData {
@@ -319,7 +333,6 @@ class ComposeViewModel
return completedCaptioningLiveData
}
-
fun searchAutocompleteSuggestions(token: String): List {
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?) {
+
+ if (setupComplete.value == true) {
+ return
+ }
+
val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
@@ -385,6 +396,7 @@ class ComposeViewModel
preferredVisibility.num.coerceAtLeast(replyVisibility.num))
inReplyToId = composeOptions?.inReplyToId
+
modifiedInitialState = composeOptions?.modifiedInitialState == true
val contentWarning = composeOptions?.contentWarning
@@ -396,10 +408,11 @@ class ComposeViewModel
}
// recreate media list
- // when coming from SavedTootActivity
val loadedDraftMediaUris = composeOptions?.mediaUrls
val loadedDraftMediaDescriptions: List? = composeOptions?.mediaDescriptions
+ val draftAttachments = composeOptions?.draftAttachments
if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) {
+ // when coming from SavedTootActivity
loadedDraftMediaUris.zip(loadedDraftMediaDescriptions)
.forEach { (uri, description) ->
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 ->
- // when coming from redraft
+ // when coming from redraft or ScheduledTootActivity
val mediaType = when (a.type) {
Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO
Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE
Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO
- else -> QueuedMedia.Type.IMAGE
}
addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description)
}
-
savedTootUid = composeOptions?.savedTootUid ?: 0
- scheduledTootUid = composeOptions?.scheduledTootUid
+ draftId = composeOptions?.draftId ?: 0
+ scheduledTootId = composeOptions?.scheduledTootId
startingText = composeOptions?.tootText
-
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
startingVisibility = tootVisibility
@@ -441,7 +455,6 @@ class ComposeViewModel
startingText = builder.toString()
}
-
scheduledAt.value = composeOptions?.scheduledAt
composeOptions?.sensitive?.let { markMediaAsSensitive.value = it }
@@ -462,6 +475,13 @@ class ComposeViewModel
scheduledAt.value = newScheduledAt
}
+ override fun onCleared() {
+ for (uploadDisposable in mediaToDisposable.values) {
+ uploadDisposable.dispose()
+ }
+ super.onCleared()
+ }
+
private companion object {
const val TAG = "ComposeViewModel"
}
@@ -479,4 +499,9 @@ data class ComposeInstanceParams(
val pollMaxOptions: Int,
val pollMaxLength: Int,
val supportsScheduled: Boolean
-)
\ No newline at end of file
+)
+
+/**
+ * Thrown when trying to add an image when video is already present or the other way around
+ */
+class VideoOrImageException : Exception()
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt
index 1fa03d54b..8ff7dcf32 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt
@@ -173,7 +173,13 @@ class MediaUploaderImpl(
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 ->
emitter.onNext(UploadEvent.FinishedEvent(attachment))
emitter.onComplete()
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt
new file mode 100644
index 000000000..6f5f9005a
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt
@@ -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 . */
+
+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,
+ mediaDescriptions: List,
+ 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 = 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)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt
new file mode 100644
index 000000000..69403fdb5
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt
@@ -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 . */
+
+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(
+ object: DiffUtil.ItemCallback() {
+ 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()
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt
new file mode 100644
index 000000000..ddf8a8385
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt
@@ -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 . */
+
+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
+
+ 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)
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt
new file mode 100644
index 000000000..5dfbceac8
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt
@@ -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 . */
+
+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>(
+ object : DiffUtil.ItemCallback() {
+ 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 {
+
+ 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, 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()
+ }
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt
new file mode 100644
index 000000000..9eca963aa
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt
@@ -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 . */
+
+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 = mutableListOf()
+
+ fun showOldDraftsButton(): Observable {
+ 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 {
+ return api.statusSingle(tootId)
+ }
+
+ override fun onCleared() {
+ deletedDrafts.forEach {
+ draftHelper.deleteAttachments(it).subscribe()
+ }
+ }
+
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt
index 18b04df52..f0944a347 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt
@@ -120,7 +120,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
override fun edit(item: ScheduledStatus) {
val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions(
- scheduledTootUid = item.id,
+ scheduledTootId = item.id,
tootText = item.params.text,
contentWarning = item.params.spoilerText,
mediaAttachments = item.mediaAttachments,
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
index 5c5b7cb62..d35fd3891 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
+++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
@@ -28,9 +28,9 @@ import com.keylesspalace.tusky.components.conversation.ConversationEntity;
* 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
- }, version = 24)
+ }, version = 25)
public abstract class AppDatabase extends RoomDatabase {
public abstract TootDao tootDao();
@@ -38,6 +38,7 @@ public abstract class AppDatabase extends RoomDatabase {
public abstract InstanceDao instanceDao();
public abstract ConversationsDao conversationDao();
public abstract TimelineDao timelineDao();
+ public abstract DraftDao draftDao();
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
@@ -46,7 +47,6 @@ public abstract class AppDatabase extends RoomDatabase {
database.execSQL("INSERT INTO TootEntity2 SELECT * FROM TootEntity;");
database.execSQL("DROP TABLE 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)"
+ );
+ }
+ };
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt
index 3492deda8..1b1f94f3d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt
@@ -24,10 +24,7 @@ import com.google.gson.reflect.TypeToken
import com.keylesspalace.tusky.TabData
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
import com.keylesspalace.tusky.createTabDataFromId
-import com.keylesspalace.tusky.entity.Attachment
-import com.keylesspalace.tusky.entity.Emoji
-import com.keylesspalace.tusky.entity.Poll
-import com.keylesspalace.tusky.entity.Status
+import com.keylesspalace.tusky.entity.*
import com.keylesspalace.tusky.json.SpannedTypeAdapter
import com.keylesspalace.tusky.util.trimTrailingWhitespace
import java.net.URLDecoder
@@ -151,4 +148,23 @@ class Converters {
return gson.fromJson(pollJson, Poll::class.java)
}
-}
\ No newline at end of file
+ @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?): String? {
+ return gson.toJson(draftAttachments)
+ }
+
+ @TypeConverter
+ fun jsonToDraftAttachmentList(draftAttachmentListJson: String?): List? {
+ return gson.fromJson(draftAttachmentListJson, object : TypeToken>() {}.type)
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt
new file mode 100644
index 000000000..105fd7c5a
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt
@@ -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 . */
+
+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
+
+ @Query("DELETE FROM DraftEntity WHERE id = :id")
+ fun delete(id: Int): Completable
+
+ @Query("SELECT * FROM DraftEntity WHERE id = :id")
+ fun find(id: Int): Single
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt
new file mode 100644
index 000000000..be1eca589
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt
@@ -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 . */
+
+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,
+ 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;
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt
index da98cb4b3..296111d30 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt
@@ -26,7 +26,7 @@ import com.keylesspalace.tusky.entity.Status
// Avoiding rescanning status table when accounts table changes. Recommended by Room(c).
indices = [Index("authorServerId", "timelineUserId")]
)
-@TypeConverters(TootEntity.Converters::class)
+@TypeConverters(Converters::class)
data class TimelineStatusEntity(
val serverId: String, // id never flips: we need it for sorting so it's a real id
val url: String?,
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java b/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java
index c121e1705..f46c2753a 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java
+++ b/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java
@@ -16,12 +16,12 @@
package com.keylesspalace.tusky.db;
import androidx.room.Dao;
-import androidx.room.Insert;
-import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import java.util.List;
+import io.reactivex.Observable;
+
/**
* Created by cto3543 on 28/06/2017.
*
@@ -30,8 +30,6 @@ import java.util.List;
@Dao
public interface TootDao {
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- void insertOrReplace(TootEntity users);
@Query("SELECT * FROM TootEntity ORDER BY uid DESC")
List loadAll();
@@ -41,4 +39,7 @@ public interface TootDao {
@Query("SELECT * FROM TootEntity WHERE uid = :uid")
TootEntity find(int uid);
-}
+
+ @Query("SELECT COUNT(*) FROM TootEntity")
+ Observable savedTootCount();
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
index 0257c28f6..2e82d6407 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
@@ -18,6 +18,7 @@ package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.*
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
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.preference.PreferencesActivity
import com.keylesspalace.tusky.components.report.ReportActivity
@@ -107,4 +108,7 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector
abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity
+
+ @ContributesAndroidInjector
+ abstract fun contributesDraftActivity(): DraftsActivity
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
index 7e86bbac5..1137b12b0 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
@@ -80,7 +80,7 @@ class AppModule {
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_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()
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
index c461012db..ce83deda8 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
@@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider
import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel
import com.keylesspalace.tusky.components.compose.ComposeViewModel
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.scheduled.ScheduledTootViewModel
import com.keylesspalace.tusky.components.search.SearchViewModel
@@ -91,5 +92,10 @@ abstract class ViewModelModule {
@ViewModelKey(AnnouncementsViewModel::class)
internal abstract fun announcementsViewModel(viewModel: AnnouncementsViewModel): ViewModel
+ @Binds
+ @IntoMap
+ @ViewModelKey(DraftsViewModel::class)
+ internal abstract fun draftsViewModel(viewModel: DraftsViewModel): ViewModel
+
//Add more ViewModels here
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
index 7b79e1b54..58caec85e 100644
--- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
@@ -124,7 +124,8 @@ interface MastodonApi {
@Multipart
@POST("api/v1/media")
fun uploadMedia(
- @Part file: MultipartBody.Part
+ @Part file: MultipartBody.Part,
+ @Part description: MultipartBody.Part? = null
): Single
@FormUrlEncoded
@@ -147,6 +148,11 @@ interface MastodonApi {
@Path("id") statusId: String
): Call
+ @GET("api/v1/statuses/{id}")
+ fun statusSingle(
+ @Path("id") statusId: String
+ ): Single
+
@GET("api/v1/statuses/{id}/context")
fun statusContext(
@Path("id") statusId: String
diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt
index f8bf81b30..911f58c1b 100644
--- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt
@@ -60,7 +60,6 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
val notificationManager = NotificationManagerCompat.from(context)
-
if (intent.action == NotificationHelper.REPLY_ACTION) {
val message = getReplyMessage(intent)
@@ -89,22 +88,23 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
val sendIntent = SendTootService.sendTootIntent(
context,
TootToSend(
- text,
- spoiler,
- visibility.serverString(),
- false,
- emptyList(),
- emptyList(),
- emptyList(),
- null,
- citedStatusId,
- null,
- null,
- null,
- null, account.id,
- 0,
- randomAlphanumericString(16),
- 0
+ text = text,
+ warningText = spoiler,
+ visibility = visibility.serverString(),
+ sensitive = false,
+ mediaIds = emptyList(),
+ mediaUris = emptyList(),
+ mediaDescriptions = emptyList(),
+ scheduledAt = null,
+ inReplyToId = citedStatusId,
+ poll = null,
+ replyingStatusContent = null,
+ replyingStatusAuthorUsername = null,
+ accountId = account.id,
+ savedTootUid = -1,
+ draftId = -1,
+ idempotencyKey = randomAlphanumericString(16),
+ retries = 0
)
)
diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt
index 328265b5e..b54a941b1 100644
--- a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt
@@ -18,6 +18,7 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.StatusComposedEvent
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
+import com.keylesspalace.tusky.components.drafts.DraftHelper
import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.di.Injectable
@@ -46,7 +47,8 @@ class SendTootService : Service(), Injectable {
lateinit var eventHub: EventHub
@Inject
lateinit var database: AppDatabase
-
+ @Inject
+ lateinit var draftHelper: DraftHelper
@Inject
lateinit var saveTootHelper: SaveTootHelper
@@ -163,6 +165,10 @@ class SendTootService : Service(), Injectable {
if (tootToSend.savedTootUid != 0) {
saveTootHelper.deleteDraft(tootToSend.savedTootUid)
}
+ if (tootToSend.draftId != 0) {
+ draftHelper.deleteDraftAndAttachments(tootToSend.draftId)
+ .subscribe()
+ }
if (scheduled) {
response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch)
@@ -245,17 +251,19 @@ class SendTootService : Service(), Injectable {
private fun saveTootToDrafts(toot: TootToSend) {
- saveTootHelper.saveToot(toot.text,
- toot.warningText,
- toot.savedJsonUrls,
- toot.mediaUris,
- toot.mediaDescriptions,
- toot.savedTootUid,
- toot.inReplyToId,
- toot.replyingStatusContent,
- toot.replyingStatusAuthorUsername,
- Status.Visibility.byString(toot.visibility),
- toot.poll)
+ draftHelper.saveDraft(
+ draftId = toot.draftId,
+ accountId = toot.accountId,
+ inReplyToId = toot.inReplyToId,
+ content = toot.text,
+ contentWarning = toot.warningText,
+ sensitive = toot.sensitive,
+ visibility = Status.Visibility.byString(toot.visibility),
+ mediaUris = toot.mediaUris,
+ mediaDescriptions = toot.mediaDescriptions,
+ poll = toot.poll,
+ failedToSend = true
+ ).subscribe()
}
private fun cancelSendingIntent(tootId: Int): PendingIntent {
@@ -323,9 +331,9 @@ data class TootToSend(
val poll: NewPoll?,
val replyingStatusContent: String?,
val replyingStatusAuthorUsername: String?,
- val savedJsonUrls: List?,
val accountId: Long,
val savedTootUid: Int,
+ val draftId: Int,
val idempotencyKey: String,
var retries: Int
) : Parcelable
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt
new file mode 100644
index 000000000..14aee81b5
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt
@@ -0,0 +1,8 @@
+package com.keylesspalace.tusky.util
+
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewbinding.ViewBinding
+
+class BindingViewHolder(
+ val binding: T
+) : RecyclerView.ViewHolder(binding.root)
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt
index 2ad4b825d..c78b0f787 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt
@@ -1,5 +1,6 @@
package com.keylesspalace.tusky.util
+import androidx.annotation.CallSuper
import androidx.lifecycle.ViewModel
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
@@ -9,6 +10,7 @@ open class RxAwareViewModel : ViewModel() {
fun Disposable.autoDispose() = disposables.add(this)
+ @CallSuper
override fun onCleared() {
super.onCleared()
disposables.clear()
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java
index 690098309..29693550d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java
+++ b/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java
@@ -1,33 +1,18 @@
package com.keylesspalace.tusky.util;
-import android.annotation.SuppressLint;
-import android.content.ContentResolver;
import android.content.Context;
import android.net.Uri;
-import android.os.AsyncTask;
-import android.text.TextUtils;
import android.util.Log;
-import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.core.content.FileProvider;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
-import com.keylesspalace.tusky.BuildConfig;
import com.keylesspalace.tusky.db.AppDatabase;
import com.keylesspalace.tusky.db.TootDao;
import com.keylesspalace.tusky.db.TootEntity;
-import com.keylesspalace.tusky.entity.NewPoll;
-import com.keylesspalace.tusky.entity.Status;
-import java.io.File;
-import java.text.SimpleDateFormat;
import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
import javax.inject.Inject;
@@ -45,61 +30,6 @@ public final class SaveTootHelper {
this.context = context;
}
- @SuppressLint("StaticFieldLeak")
- public boolean saveToot(@NonNull String content,
- @NonNull String contentWarning,
- @Nullable List savedJsonUrls,
- @NonNull List mediaUris,
- @NonNull List 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 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() {
- @Override
- protected Void doInBackground(Void... params) {
- tootDao.insertOrReplace(toot);
- return null;
- }
- }.execute();
- return true;
- }
-
public void deleteDraft(int tootId) {
TootEntity item = tootDao.find(tootId);
if (item != null) {
@@ -124,82 +54,4 @@ public final class SaveTootHelper {
tootDao.delete(item.getUid());
}
- @Nullable
- private List saveMedia(@NonNull List mediaUris,
- @Nullable List 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 filesSoFar = new ArrayList<>();
- ArrayList 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 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));
- }
- }
- }
-
- /**
- * A∖B={x∈A|x∉B}
- *
- * @return all elements of set A that are not in set B.
- */
- private static List setDifference(List a, List b) {
- List c = new ArrayList<>();
- for (String s : a) {
- if (!b.contains(s)) {
- c.add(s);
- }
- }
- return c;
- }
-
-}
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_alert_circle.xml b/app/src/main/res/drawable/ic_alert_circle.xml
new file mode 100644
index 000000000..4c894f0dc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_alert_circle.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_notebook.xml b/app/src/main/res/drawable/ic_notebook.xml
index 2395cd141..93ff78919 100644
--- a/app/src/main/res/drawable/ic_notebook.xml
+++ b/app/src/main/res/drawable/ic_notebook.xml
@@ -4,5 +4,5 @@
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout-sw640dp/fragment_view_thread.xml b/app/src/main/res/layout-sw640dp/fragment_view_thread.xml
index 52de2b95a..150f0860c 100644
--- a/app/src/main/res/layout-sw640dp/fragment_view_thread.xml
+++ b/app/src/main/res/layout-sw640dp/fragment_view_thread.xml
@@ -1,8 +1,10 @@
+ android:background="?attr/windowBackgroundColor"
+ tools:viewBindingIgnore="true">
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_view_thread.xml b/app/src/main/res/layout/fragment_view_thread.xml
index 433f1ed0f..806d420a4 100644
--- a/app/src/main/res/layout/fragment_view_thread.xml
+++ b/app/src/main/res/layout/fragment_view_thread.xml
@@ -1,9 +1,11 @@
+ android:layout_gravity="top"
+ tools:viewBindingIgnore="true">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/toolbar_basic.xml b/app/src/main/res/layout/toolbar_basic.xml
index 71039105f..47bd2d90f 100644
--- a/app/src/main/res/layout/toolbar_basic.xml
+++ b/app/src/main/res/layout/toolbar_basic.xml
@@ -1,19 +1,16 @@
-
+
-
+ android:layout_height="?attr/actionBarSize" />
-
-
-
-
-
\ No newline at end of file
+
diff --git a/app/src/main/res/menu/drafts.xml b/app/src/main/res/menu/drafts.xml
new file mode 100644
index 000000000..bbc9202f4
--- /dev/null
+++ b/app/src/main/res/menu/drafts.xml
@@ -0,0 +1,10 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml
index 835a32f8e..6f4b1cee7 100644
--- a/app/src/main/res/values-ar/strings.xml
+++ b/app/src/main/res/values-ar/strings.xml
@@ -36,7 +36,7 @@
الحسابات المحظورة
طلبات المتابعة
عدل ملفك التعريفي
- المسودات
+ المسودات
الرّخص
\@%s
شارَكَه %s
diff --git a/app/src/main/res/values-ber/strings.xml b/app/src/main/res/values-ber/strings.xml
index 2e0412ca4..9726d4ff0 100644
--- a/app/src/main/res/values-ber/strings.xml
+++ b/app/src/main/res/values-ber/strings.xml
@@ -16,7 +16,7 @@
ⵏⴰⴸⵉ
ⵣⵔⴻⴳ ⴰⵎⴰⵖⵏⵓ
ⴼⴼⴻⵖ
- ⵉⵔⴻⵡⵡⴰⵢⴻⵏ
+ ⵉⵔⴻⵡⵡⴰⵢⴻⵏ
ⵉⵙⵎⴻⵏⵢⵉⴼⴻⵏ
ⵓⵖⴰⵍ
ⴽⴻⵎⵎⴻⵍ
diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml
index 2413319b5..634f0d8f3 100644
--- a/app/src/main/res/values-bn-rBD/strings.xml
+++ b/app/src/main/res/values-bn-rBD/strings.xml
@@ -292,7 +292,7 @@
মিডিয়া লুকানো
সংবেদনশীল কন্টেন্ট
লাইসেন্সগুলি
- খসড়াগুলো
+ খসড়াগুলো
আপনার প্রোফাইল সম্পাদনা করুন
অনুরোধ অনুসরণ করুন
অবরুদ্ধ ব্যবহারকারী
diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml
index e578655ad..ae3ae2f21 100644
--- a/app/src/main/res/values-bn-rIN/strings.xml
+++ b/app/src/main/res/values-bn-rIN/strings.xml
@@ -36,7 +36,7 @@
অবরুদ্ধ ব্যবহারকারী
অনুরোধ অনুসরণ করুন
আপনার প্রোফাইল সম্পাদনা করুন
- খসড়াগুলো
+ খসড়াগুলো
লাইসেন্সগুলি
\@%s
%s সমর্থন দিয়েছে
diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml
index a769dbe80..9748a4819 100644
--- a/app/src/main/res/values-ca/strings.xml
+++ b/app/src/main/res/values-ca/strings.xml
@@ -29,7 +29,7 @@
Usuaris blocats
Peticions de seguiment
Edita el perfil
- Esborranys
+ Esborranys
\@%s
%s tootejat
Contingut sensible
diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml
index 867b3ec2c..bac6fe1a6 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -36,7 +36,7 @@
Blokovaní uživatelé
Žádosti o sledování
Upravit váš profil
- Koncepty
+ Koncepty
Licence
\@%s
%s boostnul/a
diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml
index a322a5df6..b41bbd6f9 100644
--- a/app/src/main/res/values-cy/strings.xml
+++ b/app/src/main/res/values-cy/strings.xml
@@ -32,7 +32,7 @@
Defnyddwyr wedi\'u blocio
Dilyn ceisiadau
Golygu\'ch Proffil
- Drafftiau
+ Drafftiau
Trwyddedau
%s wedi\'u hybu
Cynnwys sensitif
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 6218bbbfe..decfff34a 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -36,7 +36,7 @@
Blockierte Profile
Folgeanfragen
Dein Profil bearbeiten
- Entwürfe
+ Entwürfe
Lizenzen
\@%s
%s teilte
diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml
index e2e011087..34b5a5bc3 100644
--- a/app/src/main/res/values-eo/strings.xml
+++ b/app/src/main/res/values-eo/strings.xml
@@ -36,7 +36,7 @@
Blokitaj uzantoj
Petoj de sekvado
Redakti vian profilon
- Malnetoj
+ Malnetoj
Permesiloj
\@%s
%s diskonigis
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 089552800..2b2633a62 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -36,7 +36,7 @@
Bloqueados
Solicitudes
Editar tu perfil
- Borradores
+ Borradores
Licencias
\@%s
%s compartió
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index f4eb9e71c..0c6169582 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -32,7 +32,7 @@
Blokeatuak
Eskakizunak
Profila editatu
- Zirriborroak
+ Zirriborroak
Lizentziak
%s-(e)k bultzatu du
Kontuz edukiarekin
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 2f20b2109..2d5f570e4 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -32,7 +32,7 @@
کاربران مسدود
درخواستهای پیگیری
ویرایش نمایهتان
- پیشنویسها
+ پیشنویسها
پروانهها
%s تقویت کرد
محتوای حسّاس
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 6240d8134..74f5e1055 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -36,7 +36,7 @@
Comptes bloqués
Demandes d’abonnement
Modifier votre profil
- Brouillons
+ Brouillons
Licences
\@%s
%s a partagé
diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml
index aad2a612e..130f4abef 100644
--- a/app/src/main/res/values-ga/strings.xml
+++ b/app/src/main/res/values-ga/strings.xml
@@ -177,7 +177,7 @@
Roghanna Cuntais
Sainroghanna
Logáil Amach
- Dréachtaí
+ Dréachtaí
Roghaí
Theip ar fhíordheimhniú leis an gcás sin.
Cad is sampla ann\?
diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml
index ed594dc7e..83bfce331 100644
--- a/app/src/main/res/values-gd/strings.xml
+++ b/app/src/main/res/values-gd/strings.xml
@@ -8,7 +8,7 @@
Roighainnean cunntais
Roighainnean
Clàraich a-mach
- Dreachd
+ Dreachd
Prìomhaich
Dè a th ’ann an àite\?
Deasaich
diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml
index a4c1b373b..20b6e5d0b 100644
--- a/app/src/main/res/values-hi/strings.xml
+++ b/app/src/main/res/values-hi/strings.xml
@@ -2,7 +2,7 @@
हिंदी
पसंदीदा
- प्रारूप
+ प्रारूप
लॉग आउट
पसंद
खाता प्राथमिकताएं
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 9215ed09c..0086e2991 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -36,7 +36,7 @@
Letiltott felhasználók
Követési kérelmek
Profilod szerkesztése
- Piszkozatok
+ Piszkozatok
Licenszek
\@%s
%s megtolta
diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml
index e52cda276..7f911d96d 100644
--- a/app/src/main/res/values-is/strings.xml
+++ b/app/src/main/res/values-is/strings.xml
@@ -3,7 +3,7 @@
Skrá inn með Mastodon
Hvað er tilvik\?
Eftirlæti
- Drög
+ Drög
Skrá út
Kjörstillingar
Eiginleikar tengingar
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 3b716a1f9..c43e60f4d 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -36,7 +36,7 @@
Utenti bloccati
Richieste di seguirti
Modifica il tuo profilo
- Bozze
+ Bozze
Licenze
\@%s
%s ha boostato
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index df4ec9319..a716f733b 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -35,7 +35,7 @@
ブロックしたユーザー
フォローリクエスト
プロフィールを編集
- 下書き
+ 下書き
ライセンス
%sさんがブーストしました
閲覧注意
diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml
index 8f51de5e5..ad5a99d92 100644
--- a/app/src/main/res/values-kab/strings.xml
+++ b/app/src/main/res/values-kab/strings.xml
@@ -2,7 +2,7 @@
Qqen ɣer Maṣṭudun
Ismenyifen
- Irewwayen
+ Irewwayen
Ffeɣ
Iɣewwaṛen
Iɣewwaṛen n umiḍan
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index cf11c797f..893b4acb1 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -37,7 +37,7 @@
숨긴 도메인
팔로우 요청
프로필 편집
- 임시 저장
+ 임시 저장
라이선스
\@%s
%s님이 부스트 했습니다
diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml
index 72119eef6..de97414b7 100644
--- a/app/src/main/res/values-ml/strings.xml
+++ b/app/src/main/res/values-ml/strings.xml
@@ -3,7 +3,7 @@
മസ്റ്റഡോൺ വഴി പ്രവേശിക്കുക
എന്താണ് ഒരു ഇൻസ്റ്റൻസ്\?
പ്രിയപ്പെട്ടവ
- കരടുകൾ
+ കരടുകൾ
പുറത്തിറങ്ങുക
മുൻഗണനകൾ
അക്കൗണ്ട് മുൻഗണനകൾ
diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml
index b8d2d09da..50d94b3d7 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -36,7 +36,7 @@
Geblokkeerde gebruikers
Volgverzoeken
Profiel bewerken
- Concepten
+ Concepten
Licenties
\@%s
%s boostte
diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml
index cd633dd0e..3adcf1eb8 100644
--- a/app/src/main/res/values-no-rNB/strings.xml
+++ b/app/src/main/res/values-no-rNB/strings.xml
@@ -36,7 +36,7 @@
Blokkerte brukere
Forespørsler om følgen
Endre profilen din
- Kladder
+ Kladder
Lisenser
\@%s
%s boosted
diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml
index c22afc29d..a404fef52 100644
--- a/app/src/main/res/values-oc/strings.xml
+++ b/app/src/main/res/values-oc/strings.xml
@@ -31,7 +31,7 @@
Utilizaires blocats
Demandas d’abonament
Modificar lo perfil
- Borrolhons
+ Borrolhons
Licéncias
%s partejat
Contengut sensible
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 9082f6347..03ca93804 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -31,7 +31,7 @@
Zablokowani użytkownicy
Prośby o możliwość śledzenia
Edytuj profil
- Szkice
+ Szkice
Licencje
%s podbił
Wrażliwe treści
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 95bf8a29e..d36363808 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -34,7 +34,7 @@
Usuários bloqueados
Seguidores pendentes
Editar perfil
- Rascunhos
+ Rascunhos
Licenças
%s deu boost
Mídia sensível
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 6adb5768f..9e2cf800f 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -36,7 +36,7 @@
Список блокировки
Запросы на подписку
Редактировать профиль
- Черновики
+ Черновики
Лицензии
\@%s
%s продвинул(а)
diff --git a/app/src/main/res/values-sa/strings.xml b/app/src/main/res/values-sa/strings.xml
index 2fb217ade..18bb76015 100644
--- a/app/src/main/res/values-sa/strings.xml
+++ b/app/src/main/res/values-sa/strings.xml
@@ -36,7 +36,7 @@
\@%s
अनुज्ञापत्राणि
कालबद्धदौत्यानि
- लेखविकर्षाः
+ लेखविकर्षाः
स्वीयव्यक्तिविवरणं सम्पाद्यताम्
अनुसरणार्थमनुरोधाः
प्रच्छन्नप्रदेशाः
diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml
index 68c278706..14af6584d 100644
--- a/app/src/main/res/values-sk/strings.xml
+++ b/app/src/main/res/values-sk/strings.xml
@@ -1,7 +1,7 @@
Prihlásiť sa účtom Mastodon
- Koncepty
+ Koncepty
Odhlásiť sa
Nastavenia
Nastavenia účtu
diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml
index 70bb80beb..35b3f08ea 100644
--- a/app/src/main/res/values-sl/strings.xml
+++ b/app/src/main/res/values-sl/strings.xml
@@ -34,7 +34,7 @@
Blokirani uporabniki
Zahteve za Sledenje
Uredi svoj profil
- Osnutki
+ Osnutki
Licence
\@%s
Občutljiva vsebina
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index ff8a5a133..34eb3e8c1 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -36,7 +36,7 @@
Blockerade användare
Följarförfrågningar
Ändra din profil
- Utkast
+ Utkast
Licenser
\@%s
%s knuffade
diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml
index 2affaa29b..d726ba321 100644
--- a/app/src/main/res/values-ta/strings.xml
+++ b/app/src/main/res/values-ta/strings.xml
@@ -29,7 +29,7 @@
தடைசெய்யபட்ட பயனர்கள்
பின்பற்ற கோரிக்கை
சுயவிவரத்தை திருத்த
- வரைவுகள்
+ வரைவுகள்
%s மேலேற்றப்பட்டது
உணர்ச்சிகரமான உள்ளடக்கம்
ஊடகம் மறைக்கப்பட்டது
diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml
index 5c994db6a..dc5487a81 100644
--- a/app/src/main/res/values-th/strings.xml
+++ b/app/src/main/res/values-th/strings.xml
@@ -431,7 +431,7 @@
ตั้งค่าบัญชี
ตั้งค่า
ออกจากระบบ
- ฉบับร่าง
+ ฉบับร่าง
ชื่นชอบ
การยืนยันตัวตนกับเซิร์ฟเวอร์นั้นล้มเหลว
Instance คือ\?
diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml
index 596bafa41..684ecc21e 100644
--- a/app/src/main/res/values-tr/strings.xml
+++ b/app/src/main/res/values-tr/strings.xml
@@ -36,7 +36,7 @@
Engellenmiş kullanıcılar
Takip Etme İstekleri
Profili düzeltme
- Taslaklar
+ Taslaklar
Lisanslar
\@%s
%s yineledi
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index cb7869add..5c49c828c 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -37,7 +37,7 @@
Налаштування акаунта
Налаштування
Вийти
- Чернетки
+ Чернетки
Вподобане
Увійти
Зʼєднання…
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index 460ebb2d8..d785a1f1a 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -197,7 +197,7 @@
Cộng đồng
Thông báo
Bảng tin
- Nháp
+ Nháp
Lượt thích
Máy chủ là gì\?
Tải xem trước hình ảnh
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 03c70771a..e123fd9e5 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -36,7 +36,7 @@
被屏蔽的用户
关注请求
编辑个人资料
- 草稿
+ 草稿
开源协议
\@%s
%s 转嘟了
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index 91bed1d4c..5c50933d4 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -36,7 +36,7 @@
被封鎖的使用者
關注請求
編輯個人資料
- 草稿
+ 草稿
開源授權
\@%s
%s 轉嘟了
diff --git a/app/src/main/res/values-zh-rMO/strings.xml b/app/src/main/res/values-zh-rMO/strings.xml
index 8333ff691..538284c60 100644
--- a/app/src/main/res/values-zh-rMO/strings.xml
+++ b/app/src/main/res/values-zh-rMO/strings.xml
@@ -36,7 +36,7 @@
被封鎖的使用者
關注請求
編輯個人資料
- 草稿
+ 草稿
開源授權
\@%s
%s 轉嘟了
diff --git a/app/src/main/res/values-zh-rSG/strings.xml b/app/src/main/res/values-zh-rSG/strings.xml
index 7fe476a6d..2f45c629d 100644
--- a/app/src/main/res/values-zh-rSG/strings.xml
+++ b/app/src/main/res/values-zh-rSG/strings.xml
@@ -36,7 +36,7 @@
被屏蔽的用户
关注请求
编辑个人资料
- 草稿
+ 草稿
开源协议
\@%s
%s 转嘟了
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index f4e93299d..6d690ace9 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -36,7 +36,7 @@
被封鎖的使用者
關注請求
編輯個人資料
- 草稿
+ 草稿
開源授權
\@%s
%s 轉嘟了
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index dd4f599ca..5ec69307a 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -45,6 +45,7 @@
5dp
12dp
+ 120dp
72dp
108dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6b08043e5..7439808b8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -40,7 +40,7 @@
Hidden domains
Follow Requests
Edit your profile
- Drafts
+ Drafts
Scheduled toots
Announcements
Licenses
@@ -585,6 +585,7 @@
Wellbeing
Your private note about this account
Saved!
+
Some information that might affect your mental wellbeing will be hidden. This includes:\n\n
- Favorite/Boost/Follow notifications\n
- Favorite/Boost count on toots\n
@@ -598,4 +599,15 @@
You cannot upload more than %1$d media attachments.
Do you really want to delete the list %s?
+ This toot failed to send!
+
+
+ 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!
+
+ Old Drafts
+ Failed loading Reply information
+ Draft deleted
+ The Toot you drafted a reply to has been removed
diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt
index 7f33a9769..bd9be3b21 100644
--- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt
@@ -13,7 +13,6 @@
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see . */
-
package com.keylesspalace.tusky
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.DEFAULT_CHARACTER_LIMIT
import com.keylesspalace.tusky.components.compose.MediaUploader
+import com.keylesspalace.tusky.components.drafts.DraftHelper
import com.keylesspalace.tusky.db.*
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.entity.Account
@@ -115,6 +115,7 @@ class ComposeActivityTest {
accountManagerMock,
mock(MediaUploader::class.java),
mock(ServiceClient::class.java),
+ mock(DraftHelper::class.java),
mock(SaveTootHelper::class.java),
dbMock
)