Drafts v2 (#2032)
* cleanup warnings, reorganize some code * move ComposeAutoCompleteAdapter to compose package * composeOptions doesn't need to be a class member * add DraftsActivity and DraftsViewModel * drafts * remove unnecessary Unit in ComposeViewModel * add schema/25.json * fix db migration * drafts * cleanup code * fix compose activity rotation bug * fix media descriptions getting lost when restoring a draft * improve deleting drafts * fix ComposeActivityTest * improve draft layout for almost empty drafts * reformat code * show toast when opening reply to deleted toot * improve item_draft layout
This commit is contained in:
parent
baa915a0a3
commit
940d6d395a
|
@ -67,6 +67,9 @@ android {
|
||||||
androidExtensions {
|
androidExtensions {
|
||||||
experimental = true
|
experimental = true
|
||||||
}
|
}
|
||||||
|
buildFeatures {
|
||||||
|
viewBinding true
|
||||||
|
}
|
||||||
testOptions {
|
testOptions {
|
||||||
unitTests {
|
unitTests {
|
||||||
returnDefaultValues = true
|
returnDefaultValues = true
|
||||||
|
|
821
app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json
Normal file
821
app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json
Normal file
|
@ -0,0 +1,821 @@
|
||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 25,
|
||||||
|
"identityHash": "e2cb844862443c2c5cc884c11f120d43",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "TootEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "uid",
|
||||||
|
"columnName": "uid",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "text",
|
||||||
|
"columnName": "text",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "urls",
|
||||||
|
"columnName": "urls",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "descriptions",
|
||||||
|
"columnName": "descriptions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentWarning",
|
||||||
|
"columnName": "contentWarning",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToText",
|
||||||
|
"columnName": "inReplyToText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToUsername",
|
||||||
|
"columnName": "inReplyToUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"uid"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "DraftEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "contentWarning",
|
||||||
|
"columnName": "contentWarning",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "failedToSend",
|
||||||
|
"columnName": "failedToSend",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "AccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "domain",
|
||||||
|
"columnName": "domain",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accessToken",
|
||||||
|
"columnName": "accessToken",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isActive",
|
||||||
|
"columnName": "isActive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "profilePictureUrl",
|
||||||
|
"columnName": "profilePictureUrl",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsEnabled",
|
||||||
|
"columnName": "notificationsEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsMentioned",
|
||||||
|
"columnName": "notificationsMentioned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowed",
|
||||||
|
"columnName": "notificationsFollowed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFollowRequested",
|
||||||
|
"columnName": "notificationsFollowRequested",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsReblogged",
|
||||||
|
"columnName": "notificationsReblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFavorited",
|
||||||
|
"columnName": "notificationsFavorited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsPolls",
|
||||||
|
"columnName": "notificationsPolls",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsSubscriptions",
|
||||||
|
"columnName": "notificationsSubscriptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationSound",
|
||||||
|
"columnName": "notificationSound",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationVibration",
|
||||||
|
"columnName": "notificationVibration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationLight",
|
||||||
|
"columnName": "notificationLight",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultPostPrivacy",
|
||||||
|
"columnName": "defaultPostPrivacy",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "defaultMediaSensitivity",
|
||||||
|
"columnName": "defaultMediaSensitivity",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysShowSensitiveMedia",
|
||||||
|
"columnName": "alwaysShowSensitiveMedia",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "alwaysOpenSpoiler",
|
||||||
|
"columnName": "alwaysOpenSpoiler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mediaPreviewEnabled",
|
||||||
|
"columnName": "mediaPreviewEnabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastNotificationId",
|
||||||
|
"columnName": "lastNotificationId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "activeNotifications",
|
||||||
|
"columnName": "activeNotifications",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tabPreferences",
|
||||||
|
"columnName": "tabPreferences",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "notificationsFilter",
|
||||||
|
"columnName": "notificationsFilter",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"autoGenerate": true
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_AccountEntity_domain_accountId",
|
||||||
|
"unique": true,
|
||||||
|
"columnNames": [
|
||||||
|
"domain",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "InstanceEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "instance",
|
||||||
|
"columnName": "instance",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojiList",
|
||||||
|
"columnName": "emojiList",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maximumTootCharacters",
|
||||||
|
"columnName": "maximumTootCharacters",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptions",
|
||||||
|
"columnName": "maxPollOptions",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "maxPollOptionLength",
|
||||||
|
"columnName": "maxPollOptionLength",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "version",
|
||||||
|
"columnName": "version",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"instance"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineStatusEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorServerId",
|
||||||
|
"columnName": "authorServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToId",
|
||||||
|
"columnName": "inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "inReplyToAccountId",
|
||||||
|
"columnName": "inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "content",
|
||||||
|
"columnName": "content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogsCount",
|
||||||
|
"columnName": "reblogsCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favouritesCount",
|
||||||
|
"columnName": "favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogged",
|
||||||
|
"columnName": "reblogged",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bookmarked",
|
||||||
|
"columnName": "bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "favourited",
|
||||||
|
"columnName": "favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sensitive",
|
||||||
|
"columnName": "sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "spoilerText",
|
||||||
|
"columnName": "spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "visibility",
|
||||||
|
"columnName": "visibility",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "attachments",
|
||||||
|
"columnName": "attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mentions",
|
||||||
|
"columnName": "mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "application",
|
||||||
|
"columnName": "application",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogServerId",
|
||||||
|
"columnName": "reblogServerId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "reblogAccountId",
|
||||||
|
"columnName": "reblogAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "poll",
|
||||||
|
"columnName": "poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "muted",
|
||||||
|
"columnName": "muted",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "TimelineAccountEntity",
|
||||||
|
"onDelete": "NO ACTION",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"authorServerId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "TimelineAccountEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "serverId",
|
||||||
|
"columnName": "serverId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "timelineUserId",
|
||||||
|
"columnName": "timelineUserId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "localUsername",
|
||||||
|
"columnName": "localUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "username",
|
||||||
|
"columnName": "username",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "displayName",
|
||||||
|
"columnName": "displayName",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "avatar",
|
||||||
|
"columnName": "avatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "emojis",
|
||||||
|
"columnName": "emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "bot",
|
||||||
|
"columnName": "bot",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"serverId",
|
||||||
|
"timelineUserId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "ConversationEntity",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "accountId",
|
||||||
|
"columnName": "accountId",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "accounts",
|
||||||
|
"columnName": "accounts",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "unread",
|
||||||
|
"columnName": "unread",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.id",
|
||||||
|
"columnName": "s_id",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.url",
|
||||||
|
"columnName": "s_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToId",
|
||||||
|
"columnName": "s_inReplyToId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.inReplyToAccountId",
|
||||||
|
"columnName": "s_inReplyToAccountId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.account",
|
||||||
|
"columnName": "s_account",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.content",
|
||||||
|
"columnName": "s_content",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.createdAt",
|
||||||
|
"columnName": "s_createdAt",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.emojis",
|
||||||
|
"columnName": "s_emojis",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favouritesCount",
|
||||||
|
"columnName": "s_favouritesCount",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.favourited",
|
||||||
|
"columnName": "s_favourited",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.bookmarked",
|
||||||
|
"columnName": "s_bookmarked",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.sensitive",
|
||||||
|
"columnName": "s_sensitive",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.spoilerText",
|
||||||
|
"columnName": "s_spoilerText",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.attachments",
|
||||||
|
"columnName": "s_attachments",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.mentions",
|
||||||
|
"columnName": "s_mentions",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.showingHiddenContent",
|
||||||
|
"columnName": "s_showingHiddenContent",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.expanded",
|
||||||
|
"columnName": "s_expanded",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.collapsible",
|
||||||
|
"columnName": "s_collapsible",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.collapsed",
|
||||||
|
"columnName": "s_collapsed",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastStatus.poll",
|
||||||
|
"columnName": "s_poll",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"columnNames": [
|
||||||
|
"id",
|
||||||
|
"accountId"
|
||||||
|
],
|
||||||
|
"autoGenerate": false
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e2cb844862443c2c5cc884c11f120d43')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
|
@ -146,6 +146,7 @@
|
||||||
<activity android:name=".components.instancemute.InstanceListActivity" />
|
<activity android:name=".components.instancemute.InstanceListActivity" />
|
||||||
<activity android:name=".components.scheduled.ScheduledTootActivity" />
|
<activity android:name=".components.scheduled.ScheduledTootActivity" />
|
||||||
<activity android:name=".components.announcements.AnnouncementsActivity" />
|
<activity android:name=".components.announcements.AnnouncementsActivity" />
|
||||||
|
<activity android:name=".components.drafts.DraftsActivity" />
|
||||||
|
|
||||||
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
<receiver android:name=".receiver.NotificationClearBroadcastReceiver" />
|
||||||
<receiver
|
<receiver
|
||||||
|
|
|
@ -31,6 +31,7 @@ import android.widget.ImageView
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
import androidx.emoji.text.EmojiCompat
|
import androidx.emoji.text.EmojiCompat
|
||||||
import androidx.emoji.text.EmojiCompat.InitCallback
|
import androidx.emoji.text.EmojiCompat.InitCallback
|
||||||
|
@ -52,11 +53,13 @@ import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsRepository
|
import com.keylesspalace.tusky.components.conversation.ConversationsRepository
|
||||||
|
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
||||||
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
import com.keylesspalace.tusky.components.notifications.NotificationHelper
|
||||||
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
||||||
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
|
import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity
|
||||||
import com.keylesspalace.tusky.components.search.SearchActivity
|
import com.keylesspalace.tusky.components.search.SearchActivity
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
import com.keylesspalace.tusky.fragment.SFragment
|
import com.keylesspalace.tusky.fragment.SFragment
|
||||||
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
import com.keylesspalace.tusky.interfaces.AccountSelectionListener
|
||||||
|
@ -98,6 +101,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var conversationRepository: ConversationsRepository
|
lateinit var conversationRepository: ConversationsRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var appDb: AppDatabase
|
||||||
|
|
||||||
private lateinit var header: AccountHeaderView
|
private lateinit var header: AccountHeaderView
|
||||||
|
|
||||||
private var notificationTabPosition = 0
|
private var notificationTabPosition = 0
|
||||||
|
@ -229,6 +235,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
// Flush old media that was cached for sharing
|
// Flush old media that was cached for sharing
|
||||||
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
|
deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky"))
|
||||||
}
|
}
|
||||||
|
draftWarning()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
|
@ -397,7 +404,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
nameRes = R.string.action_access_saved_toot
|
nameRes = R.string.action_access_saved_toot
|
||||||
iconRes = R.drawable.ic_notebook
|
iconRes = R.drawable.ic_notebook
|
||||||
onClick = {
|
onClick = {
|
||||||
val intent = Intent(context, SavedTootActivity::class.java)
|
val intent = DraftsActivity.newIntent(context)
|
||||||
startActivityWithSlideInAnimation(intent)
|
startActivityWithSlideInAnimation(intent)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -741,6 +748,29 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
|
||||||
header.setActiveProfile(accountManager.activeAccount!!.id)
|
header.setActiveProfile(accountManager.activeAccount!!.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun draftWarning() {
|
||||||
|
val sharedPrefsKey = "show_draft_warning"
|
||||||
|
appDb.tootDao().savedTootCount()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||||
|
.subscribe { draftCount ->
|
||||||
|
val showDraftWarning = preferences.getBoolean(sharedPrefsKey, true)
|
||||||
|
if (draftCount > 0 && showDraftWarning) {
|
||||||
|
AlertDialog.Builder(this)
|
||||||
|
.setMessage(R.string.new_drafts_warning)
|
||||||
|
.setNegativeButton("Don't show again") { _, _ ->
|
||||||
|
preferences.edit(commit = true) {
|
||||||
|
putBoolean(sharedPrefsKey, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
override fun getActionButton(): FloatingActionButton? = composeButton
|
override fun getActionButton(): FloatingActionButton? = composeButton
|
||||||
|
|
||||||
override fun androidInjector() = androidInjector
|
override fun androidInjector() = androidInjector
|
||||||
|
|
|
@ -89,7 +89,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
|
||||||
setSupportActionBar(toolbar);
|
setSupportActionBar(toolbar);
|
||||||
ActionBar bar = getSupportActionBar();
|
ActionBar bar = getSupportActionBar();
|
||||||
if (bar != null) {
|
if (bar != null) {
|
||||||
bar.setTitle(getString(R.string.title_saved_toot));
|
bar.setTitle(getString(R.string.title_drafts));
|
||||||
bar.setDisplayHomeAsUpEnabled(true);
|
bar.setDisplayHomeAsUpEnabled(true);
|
||||||
bar.setDisplayShowHomeEnabled(true);
|
bar.setDisplayShowHomeEnabled(true);
|
||||||
}
|
}
|
||||||
|
@ -166,6 +166,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
|
||||||
ComposeOptions composeOptions = new ComposeOptions(
|
ComposeOptions composeOptions = new ComposeOptions(
|
||||||
/*scheduledTootUid*/null,
|
/*scheduledTootUid*/null,
|
||||||
item.getUid(),
|
item.getUid(),
|
||||||
|
/*drafId*/null,
|
||||||
item.getText(),
|
item.getText(),
|
||||||
jsonUrls,
|
jsonUrls,
|
||||||
descriptions,
|
descriptions,
|
||||||
|
@ -177,6 +178,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd
|
||||||
item.getInReplyToUsername(),
|
item.getInReplyToUsername(),
|
||||||
item.getInReplyToText(),
|
item.getInReplyToText(),
|
||||||
/*mediaAttachments*/null,
|
/*mediaAttachments*/null,
|
||||||
|
/*draftAttachments*/null,
|
||||||
/*scheduledAt*/null,
|
/*scheduledAt*/null,
|
||||||
/*sensitive*/null,
|
/*sensitive*/null,
|
||||||
/*poll*/null,
|
/*poll*/null,
|
||||||
|
|
|
@ -30,7 +30,6 @@ import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
|
@ -57,13 +56,13 @@ import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.BaseActivity
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
import com.keylesspalace.tusky.BuildConfig
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
import com.keylesspalace.tusky.R
|
import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter
|
|
||||||
import com.keylesspalace.tusky.adapter.EmojiAdapter
|
import com.keylesspalace.tusky.adapter.EmojiAdapter
|
||||||
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
|
import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener
|
||||||
import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog
|
import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog
|
||||||
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
|
import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog
|
||||||
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener
|
import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener
|
||||||
import com.keylesspalace.tusky.db.AccountEntity
|
import com.keylesspalace.tusky.db.AccountEntity
|
||||||
|
import com.keylesspalace.tusky.db.DraftAttachment
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.Attachment
|
||||||
|
@ -81,7 +80,6 @@ import java.io.File
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
|
@ -104,10 +102,10 @@ class ComposeActivity : BaseActivity(),
|
||||||
// this only exists when a status is trying to be sent, but uploads are still occurring
|
// this only exists when a status is trying to be sent, but uploads are still occurring
|
||||||
private var finishingUploadDialog: ProgressDialog? = null
|
private var finishingUploadDialog: ProgressDialog? = null
|
||||||
private var photoUploadUri: Uri? = null
|
private var photoUploadUri: Uri? = null
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT
|
var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT
|
||||||
|
|
||||||
private var composeOptions: ComposeOptions? = null
|
|
||||||
private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
|
private val viewModel: ComposeViewModel by viewModels { viewModelFactory }
|
||||||
|
|
||||||
private val maxUploadMediaNumber = 4
|
private val maxUploadMediaNumber = 4
|
||||||
|
@ -148,17 +146,17 @@ class ComposeActivity : BaseActivity(),
|
||||||
|
|
||||||
/* If the composer is started up as a reply to another post, override the "starting" state
|
/* If the composer is started up as a reply to another post, override the "starting" state
|
||||||
* based on what the intent from the reply request passes. */
|
* based on what the intent from the reply request passes. */
|
||||||
if (intent != null) {
|
|
||||||
this.composeOptions = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA)
|
val composeOptions: ComposeOptions? = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA)
|
||||||
viewModel.setup(composeOptions)
|
|
||||||
setupReplyViews(composeOptions?.replyingStatusAuthor)
|
viewModel.setup(composeOptions)
|
||||||
val tootText = composeOptions?.tootText
|
setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent)
|
||||||
if (!tootText.isNullOrEmpty()) {
|
val tootText = composeOptions?.tootText
|
||||||
composeEditField.setText(tootText)
|
if (!tootText.isNullOrEmpty()) {
|
||||||
}
|
composeEditField.setText(tootText)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(composeOptions?.scheduledAt)) {
|
if (!composeOptions?.scheduledAt.isNullOrEmpty()) {
|
||||||
composeScheduleView.setDateTime(composeOptions?.scheduledAt)
|
composeScheduleView.setDateTime(composeOptions?.scheduledAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,38 +167,24 @@ class ComposeActivity : BaseActivity(),
|
||||||
viewModel.setupComplete.value = true
|
viewModel.setupComplete.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun applyShareIntent(intent: Intent?, savedInstanceState: Bundle?) {
|
private fun applyShareIntent(intent: Intent, savedInstanceState: Bundle?) {
|
||||||
if (intent != null && savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
/* Get incoming images being sent through a share action from another app. Only do this
|
/* Get incoming images being sent through a share action from another app. Only do this
|
||||||
* when savedInstanceState is null, otherwise both the images from the intent and the
|
* when savedInstanceState is null, otherwise both the images from the intent and the
|
||||||
* instance state will be re-queued. */
|
* instance state will be re-queued. */
|
||||||
val type = intent.type
|
intent.type?.also { type ->
|
||||||
if (type != null) {
|
|
||||||
if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) {
|
if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) {
|
||||||
val uriList = ArrayList<Uri>()
|
when (intent.action) {
|
||||||
if (intent.action != null) {
|
Intent.ACTION_SEND -> {
|
||||||
when (intent.action) {
|
intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)?.let { uri ->
|
||||||
Intent.ACTION_SEND -> {
|
pickMedia(uri)
|
||||||
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)
|
}
|
||||||
if (uri != null) {
|
}
|
||||||
uriList.add(uri)
|
Intent.ACTION_SEND_MULTIPLE -> {
|
||||||
}
|
intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)?.forEach { uri ->
|
||||||
}
|
pickMedia(uri)
|
||||||
Intent.ACTION_SEND_MULTIPLE -> {
|
|
||||||
val list = intent.getParcelableArrayListExtra<Uri>(
|
|
||||||
Intent.EXTRA_STREAM)
|
|
||||||
if (list != null) {
|
|
||||||
for (uri in list) {
|
|
||||||
if (uri != null) {
|
|
||||||
uriList.add(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for (uri in uriList) {
|
|
||||||
pickMedia(uri)
|
|
||||||
}
|
}
|
||||||
} else if (type == "text/plain" && intent.action == Intent.ACTION_SEND) {
|
} else if (type == "text/plain" && intent.action == Intent.ACTION_SEND) {
|
||||||
|
|
||||||
|
@ -224,7 +208,7 @@ class ComposeActivity : BaseActivity(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupReplyViews(replyingStatusAuthor: String?) {
|
private fun setupReplyViews(replyingStatusAuthor: String?, replyingStatusContent: String?) {
|
||||||
if (replyingStatusAuthor != null) {
|
if (replyingStatusAuthor != null) {
|
||||||
composeReplyView.show()
|
composeReplyView.show()
|
||||||
composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor)
|
composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor)
|
||||||
|
@ -248,7 +232,7 @@ class ComposeActivity : BaseActivity(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
composeOptions?.replyingStatusContent?.let { composeReplyContentView.text = it }
|
replyingStatusContent?.let { composeReplyContentView.text = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setupContentWarningField(startingContentWarning: String?) {
|
private fun setupContentWarningField(startingContentWarning: String?) {
|
||||||
|
@ -651,7 +635,6 @@ class ComposeActivity : BaseActivity(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private fun removePoll() {
|
private fun removePoll() {
|
||||||
viewModel.poll.value = null
|
viewModel.poll.value = null
|
||||||
pollPreview.hide()
|
pollPreview.hide()
|
||||||
|
@ -835,22 +818,22 @@ class ComposeActivity : BaseActivity(),
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, intent)
|
super.onActivityResult(requestCode, resultCode, intent)
|
||||||
if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) {
|
if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) {
|
||||||
if(intent.data != null){
|
if (intent.data != null) {
|
||||||
// Single media, upload it and done.
|
// Single media, upload it and done.
|
||||||
pickMedia(intent.data!!)
|
pickMedia(intent.data!!)
|
||||||
}else if(intent.clipData != null){
|
} else if (intent.clipData != null) {
|
||||||
val clipData = intent.clipData!!
|
val clipData = intent.clipData!!
|
||||||
val count = clipData.itemCount
|
val count = clipData.itemCount
|
||||||
if(mediaCount + count > maxUploadMediaNumber){
|
if (mediaCount + count > maxUploadMediaNumber) {
|
||||||
// check if exist media + upcoming media > 4, then prob error message.
|
// check if exist media + upcoming media > 4, then prob error message.
|
||||||
Toast.makeText(this, getString(R.string.error_upload_max_media_reached, maxUploadMediaNumber), Toast.LENGTH_SHORT).show()
|
Toast.makeText(this, getString(R.string.error_upload_max_media_reached, maxUploadMediaNumber), Toast.LENGTH_SHORT).show()
|
||||||
}else{
|
} else {
|
||||||
// if not grater then 4, upload all multiple media.
|
// if not grater then 4, upload all multiple media.
|
||||||
for (i in 0 until count) {
|
for (i in 0 until count) {
|
||||||
val imageUri = clipData.getItemAt(i).getUri()
|
val imageUri = clipData.getItemAt(i).getUri()
|
||||||
pickMedia(imageUri)
|
pickMedia(imageUri)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) {
|
} else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) {
|
||||||
pickMedia(photoUploadUri!!)
|
pickMedia(photoUploadUri!!)
|
||||||
|
@ -1018,8 +1001,9 @@ class ComposeActivity : BaseActivity(),
|
||||||
@Parcelize
|
@Parcelize
|
||||||
data class ComposeOptions(
|
data class ComposeOptions(
|
||||||
// Let's keep fields var until all consumers are Kotlin
|
// Let's keep fields var until all consumers are Kotlin
|
||||||
var scheduledTootUid: String? = null,
|
var scheduledTootId: String? = null,
|
||||||
var savedTootUid: Int? = null,
|
var savedTootUid: Int? = null,
|
||||||
|
var draftId: Int? = null,
|
||||||
var tootText: String? = null,
|
var tootText: String? = null,
|
||||||
var mediaUrls: List<String>? = null,
|
var mediaUrls: List<String>? = null,
|
||||||
var mediaDescriptions: List<String>? = null,
|
var mediaDescriptions: List<String>? = null,
|
||||||
|
@ -1031,6 +1015,7 @@ class ComposeActivity : BaseActivity(),
|
||||||
var replyingStatusAuthor: String? = null,
|
var replyingStatusAuthor: String? = null,
|
||||||
var replyingStatusContent: String? = null,
|
var replyingStatusContent: String? = null,
|
||||||
var mediaAttachments: List<Attachment>? = null,
|
var mediaAttachments: List<Attachment>? = null,
|
||||||
|
var draftAttachments: List<DraftAttachment>? = null,
|
||||||
var scheduledAt: String? = null,
|
var scheduledAt: String? = null,
|
||||||
var sensitive: Boolean? = null,
|
var sensitive: Boolean? = null,
|
||||||
var poll: NewPoll? = null,
|
var poll: NewPoll? = null,
|
||||||
|
@ -1057,7 +1042,6 @@ class ComposeActivity : BaseActivity(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun canHandleMimeType(mimeType: String?): Boolean {
|
fun canHandleMimeType(mimeType: String?): Boolean {
|
||||||
return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain")
|
return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain")
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
package com.keylesspalace.tusky.adapter;
|
package com.keylesspalace.tusky.components.compose;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
|
@ -21,8 +21,8 @@ import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.Observer
|
import androidx.lifecycle.Observer
|
||||||
import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter
|
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia
|
||||||
|
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||||
import com.keylesspalace.tusky.components.search.SearchType
|
import com.keylesspalace.tusky.components.search.SearchType
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
|
@ -39,18 +39,12 @@ import io.reactivex.rxkotlin.Singles
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
/**
|
class ComposeViewModel @Inject constructor(
|
||||||
* Throw when trying to add an image when video is already present or the other way around
|
|
||||||
*/
|
|
||||||
class VideoOrImageException : Exception()
|
|
||||||
|
|
||||||
|
|
||||||
class ComposeViewModel
|
|
||||||
@Inject constructor(
|
|
||||||
private val api: MastodonApi,
|
private val api: MastodonApi,
|
||||||
private val accountManager: AccountManager,
|
private val accountManager: AccountManager,
|
||||||
private val mediaUploader: MediaUploader,
|
private val mediaUploader: MediaUploader,
|
||||||
private val serviceClient: ServiceClient,
|
private val serviceClient: ServiceClient,
|
||||||
|
private val draftHelper: DraftHelper,
|
||||||
private val saveTootHelper: SaveTootHelper,
|
private val saveTootHelper: SaveTootHelper,
|
||||||
private val db: AppDatabase
|
private val db: AppDatabase
|
||||||
) : RxAwareViewModel() {
|
) : RxAwareViewModel() {
|
||||||
|
@ -59,7 +53,8 @@ class ComposeViewModel
|
||||||
private var replyingStatusContent: String? = null
|
private var replyingStatusContent: String? = null
|
||||||
internal var startingText: String? = null
|
internal var startingText: String? = null
|
||||||
private var savedTootUid: Int = 0
|
private var savedTootUid: Int = 0
|
||||||
private var scheduledTootUid: String? = null
|
private var draftId: Int = 0
|
||||||
|
private var scheduledTootId: String? = null
|
||||||
private var startingContentWarning: String = ""
|
private var startingContentWarning: String = ""
|
||||||
private var inReplyToId: String? = null
|
private var inReplyToId: String? = null
|
||||||
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
|
private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN
|
||||||
|
@ -81,10 +76,6 @@ class ComposeViewModel
|
||||||
val markMediaAsSensitive =
|
val markMediaAsSensitive =
|
||||||
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false)
|
||||||
|
|
||||||
fun toggleMarkSensitive() {
|
|
||||||
this.markMediaAsSensitive.value = !this.markMediaAsSensitive.value!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN)
|
val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN)
|
||||||
val showContentWarning = mutableLiveData(false)
|
val showContentWarning = mutableLiveData(false)
|
||||||
val setupComplete = mutableLiveData(false)
|
val setupComplete = mutableLiveData(false)
|
||||||
|
@ -96,7 +87,7 @@ class ComposeViewModel
|
||||||
|
|
||||||
private val mediaToDisposable = mutableMapOf<Long, Disposable>()
|
private val mediaToDisposable = mutableMapOf<Long, Disposable>()
|
||||||
|
|
||||||
private val isEditingScheduledToot get() = !scheduledTootUid.isNullOrEmpty()
|
private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
|
||||||
|
@ -116,7 +107,7 @@ class ComposeViewModel
|
||||||
.onErrorResumeNext(
|
.onErrorResumeNext(
|
||||||
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!)
|
||||||
)
|
)
|
||||||
.subscribe ({ instanceEntity ->
|
.subscribe({ instanceEntity ->
|
||||||
emoji.postValue(instanceEntity.emojiList)
|
emoji.postValue(instanceEntity.emojiList)
|
||||||
instance.postValue(instanceEntity)
|
instance.postValue(instanceEntity)
|
||||||
}, { throwable ->
|
}, { throwable ->
|
||||||
|
@ -126,7 +117,7 @@ class ComposeViewModel
|
||||||
.autoDispose()
|
.autoDispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun pickMedia(uri: Uri): LiveData<Either<Throwable, QueuedMedia>> {
|
fun pickMedia(uri: Uri, description: String? = null): LiveData<Either<Throwable, QueuedMedia>> {
|
||||||
// We are not calling .toLiveData() here because we don't want to stop the process when
|
// We are not calling .toLiveData() here because we don't want to stop the process when
|
||||||
// the Activity goes away temporarily (like on screen rotation).
|
// the Activity goes away temporarily (like on screen rotation).
|
||||||
val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>()
|
val liveData = MutableLiveData<Either<Throwable, QueuedMedia>>()
|
||||||
|
@ -138,7 +129,7 @@ class ComposeViewModel
|
||||||
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) {
|
&& mediaItems[0].type == QueuedMedia.Type.IMAGE) {
|
||||||
throw VideoOrImageException()
|
throw VideoOrImageException()
|
||||||
} else {
|
} else {
|
||||||
addMediaToQueue(type, uri, size)
|
addMediaToQueue(type, uri, size, description)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.subscribe({ queuedMedia ->
|
.subscribe({ queuedMedia ->
|
||||||
|
@ -150,12 +141,23 @@ class ComposeViewModel
|
||||||
return liveData
|
return liveData
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addMediaToQueue(type: QueuedMedia.Type, uri: Uri, mediaSize: Long): QueuedMedia {
|
private fun addMediaToQueue(
|
||||||
val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, mediaSize)
|
type: QueuedMedia.Type,
|
||||||
|
uri: Uri,
|
||||||
|
mediaSize: Long,
|
||||||
|
description: String? = null
|
||||||
|
): QueuedMedia {
|
||||||
|
val mediaItem = QueuedMedia(
|
||||||
|
localId = System.currentTimeMillis(),
|
||||||
|
uri = uri,
|
||||||
|
type = type,
|
||||||
|
mediaSize = mediaSize,
|
||||||
|
description = description
|
||||||
|
)
|
||||||
media.value = media.value!! + mediaItem
|
media.value = media.value!! + mediaItem
|
||||||
mediaToDisposable[mediaItem.localId] = mediaUploader
|
mediaToDisposable[mediaItem.localId] = mediaUploader
|
||||||
.uploadMedia(mediaItem)
|
.uploadMedia(mediaItem)
|
||||||
.subscribe ({ event ->
|
.subscribe({ event ->
|
||||||
val item = media.value?.find { it.localId == mediaItem.localId }
|
val item = media.value?.find { it.localId == mediaItem.localId }
|
||||||
?: return@subscribe
|
?: return@subscribe
|
||||||
val newMediaItem = when (event) {
|
val newMediaItem = when (event) {
|
||||||
|
@ -190,6 +192,10 @@ class ComposeViewModel
|
||||||
media.value = media.value!!.withoutFirstWhich { it.localId == item.localId }
|
media.value = media.value!!.withoutFirstWhich { it.localId == item.localId }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun toggleMarkSensitive() {
|
||||||
|
this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true
|
||||||
|
}
|
||||||
|
|
||||||
fun didChange(content: String?, contentWarning: String?): Boolean {
|
fun didChange(content: String?, contentWarning: String?): Boolean {
|
||||||
|
|
||||||
val textChanged = !(content.isNullOrEmpty()
|
val textChanged = !(content.isNullOrEmpty()
|
||||||
|
@ -210,29 +216,37 @@ class ComposeViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteDraft() {
|
fun deleteDraft() {
|
||||||
saveTootHelper.deleteDraft(this.savedTootUid)
|
if (savedTootUid != 0) {
|
||||||
|
saveTootHelper.deleteDraft(savedTootUid)
|
||||||
|
}
|
||||||
|
if (draftId != 0) {
|
||||||
|
draftHelper.deleteDraftAndAttachments(draftId)
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveDraft(content: String, contentWarning: String) {
|
fun saveDraft(content: String, contentWarning: String) {
|
||||||
val mediaUris = mutableListOf<String>()
|
|
||||||
val mediaDescriptions = mutableListOf<String?>()
|
val mediaUris: MutableList<String> = mutableListOf()
|
||||||
for (item in media.value!!) {
|
val mediaDescriptions: MutableList<String?> = mutableListOf()
|
||||||
|
media.value?.forEach { item ->
|
||||||
mediaUris.add(item.uri.toString())
|
mediaUris.add(item.uri.toString())
|
||||||
mediaDescriptions.add(item.description)
|
mediaDescriptions.add(item.description)
|
||||||
}
|
}
|
||||||
saveTootHelper.saveToot(
|
|
||||||
content,
|
draftHelper.saveDraft(
|
||||||
contentWarning,
|
draftId = draftId,
|
||||||
null,
|
accountId = accountManager.activeAccount?.id!!,
|
||||||
mediaUris,
|
inReplyToId = inReplyToId,
|
||||||
mediaDescriptions,
|
content = content,
|
||||||
savedTootUid,
|
contentWarning = contentWarning,
|
||||||
inReplyToId,
|
sensitive = markMediaAsSensitive.value!!,
|
||||||
replyingStatusContent,
|
visibility = statusVisibility.value!!,
|
||||||
replyingStatusAuthor,
|
mediaUris = mediaUris,
|
||||||
statusVisibility.value!!,
|
mediaDescriptions = mediaDescriptions,
|
||||||
poll.value
|
poll = poll.value,
|
||||||
)
|
failedToSend = false
|
||||||
|
).subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -246,7 +260,7 @@ class ComposeViewModel
|
||||||
): LiveData<Unit> {
|
): LiveData<Unit> {
|
||||||
|
|
||||||
val deletionObservable = if (isEditingScheduledToot) {
|
val deletionObservable = if (isEditingScheduledToot) {
|
||||||
api.deleteScheduledStatus(scheduledTootUid.toString()).toObservable().map { Unit }
|
api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { }
|
||||||
} else {
|
} else {
|
||||||
just(Unit)
|
just(Unit)
|
||||||
}.toLiveData()
|
}.toLiveData()
|
||||||
|
@ -257,28 +271,30 @@ class ComposeViewModel
|
||||||
val mediaIds = ArrayList<String>()
|
val mediaIds = ArrayList<String>()
|
||||||
val mediaUris = ArrayList<Uri>()
|
val mediaUris = ArrayList<Uri>()
|
||||||
val mediaDescriptions = ArrayList<String>()
|
val mediaDescriptions = ArrayList<String>()
|
||||||
|
val mediaTypes = ArrayList<QueuedMedia.Type>()
|
||||||
for (item in media.value!!) {
|
for (item in media.value!!) {
|
||||||
mediaIds.add(item.id!!)
|
mediaIds.add(item.id!!)
|
||||||
mediaUris.add(item.uri)
|
mediaUris.add(item.uri)
|
||||||
mediaDescriptions.add(item.description ?: "")
|
mediaDescriptions.add(item.description ?: "")
|
||||||
|
mediaTypes.add(item.type)
|
||||||
}
|
}
|
||||||
|
|
||||||
val tootToSend = TootToSend(
|
val tootToSend = TootToSend(
|
||||||
content,
|
text = content,
|
||||||
spoilerText,
|
warningText = spoilerText,
|
||||||
statusVisibility.value!!.serverString(),
|
visibility = statusVisibility.value!!.serverString(),
|
||||||
mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
|
sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!),
|
||||||
mediaIds,
|
mediaIds = mediaIds,
|
||||||
mediaUris.map { it.toString() },
|
mediaUris = mediaUris.map { it.toString() },
|
||||||
mediaDescriptions,
|
mediaDescriptions = mediaDescriptions,
|
||||||
scheduledAt = scheduledAt.value,
|
scheduledAt = scheduledAt.value,
|
||||||
inReplyToId = inReplyToId,
|
inReplyToId = inReplyToId,
|
||||||
poll = poll.value,
|
poll = poll.value,
|
||||||
replyingStatusContent = null,
|
replyingStatusContent = null,
|
||||||
replyingStatusAuthorUsername = null,
|
replyingStatusAuthorUsername = null,
|
||||||
savedJsonUrls = null,
|
|
||||||
accountId = accountManager.activeAccount!!.id,
|
accountId = accountManager.activeAccount!!.id,
|
||||||
savedTootUid = 0,
|
savedTootUid = savedTootUid,
|
||||||
|
draftId = draftId,
|
||||||
idempotencyKey = randomAlphanumericString(16),
|
idempotencyKey = randomAlphanumericString(16),
|
||||||
retries = 0
|
retries = 0
|
||||||
)
|
)
|
||||||
|
@ -286,9 +302,7 @@ class ComposeViewModel
|
||||||
serviceClient.sendToot(tootToSend)
|
serviceClient.sendToot(tootToSend)
|
||||||
}
|
}
|
||||||
|
|
||||||
return combineLiveData(deletionObservable, sendObservable) { _, _ -> Unit }
|
return combineLiveData(deletionObservable, sendObservable) { _, _ -> }
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateDescription(localId: Long, description: String): LiveData<Boolean> {
|
fun updateDescription(localId: Long, description: String): LiveData<Boolean> {
|
||||||
|
@ -319,7 +333,6 @@ class ComposeViewModel
|
||||||
return completedCaptioningLiveData
|
return completedCaptioningLiveData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
fun searchAutocompleteSuggestions(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> {
|
||||||
when (token[0]) {
|
when (token[0]) {
|
||||||
'@' -> {
|
'@' -> {
|
||||||
|
@ -370,14 +383,12 @@ class ComposeViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
for (uploadDisposable in mediaToDisposable.values) {
|
|
||||||
uploadDisposable.dispose()
|
|
||||||
}
|
|
||||||
super.onCleared()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setup(composeOptions: ComposeActivity.ComposeOptions?) {
|
fun setup(composeOptions: ComposeActivity.ComposeOptions?) {
|
||||||
|
|
||||||
|
if (setupComplete.value == true) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy
|
val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy
|
||||||
|
|
||||||
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
|
val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN
|
||||||
|
@ -385,6 +396,7 @@ class ComposeViewModel
|
||||||
preferredVisibility.num.coerceAtLeast(replyVisibility.num))
|
preferredVisibility.num.coerceAtLeast(replyVisibility.num))
|
||||||
|
|
||||||
inReplyToId = composeOptions?.inReplyToId
|
inReplyToId = composeOptions?.inReplyToId
|
||||||
|
|
||||||
modifiedInitialState = composeOptions?.modifiedInitialState == true
|
modifiedInitialState = composeOptions?.modifiedInitialState == true
|
||||||
|
|
||||||
val contentWarning = composeOptions?.contentWarning
|
val contentWarning = composeOptions?.contentWarning
|
||||||
|
@ -396,10 +408,11 @@ class ComposeViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
// recreate media list
|
// recreate media list
|
||||||
// when coming from SavedTootActivity
|
|
||||||
val loadedDraftMediaUris = composeOptions?.mediaUrls
|
val loadedDraftMediaUris = composeOptions?.mediaUrls
|
||||||
val loadedDraftMediaDescriptions: List<String?>? = composeOptions?.mediaDescriptions
|
val loadedDraftMediaDescriptions: List<String?>? = composeOptions?.mediaDescriptions
|
||||||
|
val draftAttachments = composeOptions?.draftAttachments
|
||||||
if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) {
|
if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) {
|
||||||
|
// when coming from SavedTootActivity
|
||||||
loadedDraftMediaUris.zip(loadedDraftMediaDescriptions)
|
loadedDraftMediaUris.zip(loadedDraftMediaDescriptions)
|
||||||
.forEach { (uri, description) ->
|
.forEach { (uri, description) ->
|
||||||
pickMedia(uri.toUri()).observeForever { errorOrItem ->
|
pickMedia(uri.toUri()).observeForever { errorOrItem ->
|
||||||
|
@ -408,23 +421,24 @@ class ComposeViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (draftAttachments != null) {
|
||||||
|
// when coming from DraftActivity
|
||||||
|
draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) }
|
||||||
} else composeOptions?.mediaAttachments?.forEach { a ->
|
} else composeOptions?.mediaAttachments?.forEach { a ->
|
||||||
// when coming from redraft
|
// when coming from redraft or ScheduledTootActivity
|
||||||
val mediaType = when (a.type) {
|
val mediaType = when (a.type) {
|
||||||
Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO
|
Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO
|
||||||
Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE
|
Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE
|
||||||
Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO
|
Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO
|
||||||
else -> QueuedMedia.Type.IMAGE
|
|
||||||
}
|
}
|
||||||
addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description)
|
addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
savedTootUid = composeOptions?.savedTootUid ?: 0
|
savedTootUid = composeOptions?.savedTootUid ?: 0
|
||||||
scheduledTootUid = composeOptions?.scheduledTootUid
|
draftId = composeOptions?.draftId ?: 0
|
||||||
|
scheduledTootId = composeOptions?.scheduledTootId
|
||||||
startingText = composeOptions?.tootText
|
startingText = composeOptions?.tootText
|
||||||
|
|
||||||
|
|
||||||
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
|
val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN
|
||||||
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
|
if (tootVisibility.num != Status.Visibility.UNKNOWN.num) {
|
||||||
startingVisibility = tootVisibility
|
startingVisibility = tootVisibility
|
||||||
|
@ -441,7 +455,6 @@ class ComposeViewModel
|
||||||
startingText = builder.toString()
|
startingText = builder.toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
scheduledAt.value = composeOptions?.scheduledAt
|
scheduledAt.value = composeOptions?.scheduledAt
|
||||||
|
|
||||||
composeOptions?.sensitive?.let { markMediaAsSensitive.value = it }
|
composeOptions?.sensitive?.let { markMediaAsSensitive.value = it }
|
||||||
|
@ -462,6 +475,13 @@ class ComposeViewModel
|
||||||
scheduledAt.value = newScheduledAt
|
scheduledAt.value = newScheduledAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
for (uploadDisposable in mediaToDisposable.values) {
|
||||||
|
uploadDisposable.dispose()
|
||||||
|
}
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
const val TAG = "ComposeViewModel"
|
const val TAG = "ComposeViewModel"
|
||||||
}
|
}
|
||||||
|
@ -479,4 +499,9 @@ data class ComposeInstanceParams(
|
||||||
val pollMaxOptions: Int,
|
val pollMaxOptions: Int,
|
||||||
val pollMaxLength: Int,
|
val pollMaxLength: Int,
|
||||||
val supportsScheduled: Boolean
|
val supportsScheduled: Boolean
|
||||||
)
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Thrown when trying to add an image when video is already present or the other way around
|
||||||
|
*/
|
||||||
|
class VideoOrImageException : Exception()
|
|
@ -173,7 +173,13 @@ class MediaUploaderImpl(
|
||||||
|
|
||||||
val body = MultipartBody.Part.createFormData("file", filename, fileBody)
|
val body = MultipartBody.Part.createFormData("file", filename, fileBody)
|
||||||
|
|
||||||
val uploadDisposable = mastodonApi.uploadMedia(body)
|
val description = if (media.description != null) {
|
||||||
|
MultipartBody.Part.createFormData("description", media.description)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val uploadDisposable = mastodonApi.uploadMedia(body, description)
|
||||||
.subscribe({ attachment ->
|
.subscribe({ attachment ->
|
||||||
emitter.onNext(UploadEvent.FinishedEvent(attachment))
|
emitter.onNext(UploadEvent.FinishedEvent(attachment))
|
||||||
emitter.onComplete()
|
emitter.onComplete()
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
/* Copyright 2021 Tusky Contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.components.drafts
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import android.webkit.MimeTypeMap
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.keylesspalace.tusky.BuildConfig
|
||||||
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
|
import com.keylesspalace.tusky.db.DraftAttachment
|
||||||
|
import com.keylesspalace.tusky.db.DraftEntity
|
||||||
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.util.IOUtils
|
||||||
|
import io.reactivex.Completable
|
||||||
|
import io.reactivex.Single
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DraftHelper @Inject constructor(
|
||||||
|
val context: Context,
|
||||||
|
db: AppDatabase
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val draftDao = db.draftDao()
|
||||||
|
|
||||||
|
fun saveDraft(
|
||||||
|
draftId: Int,
|
||||||
|
accountId: Long,
|
||||||
|
inReplyToId: String?,
|
||||||
|
content: String?,
|
||||||
|
contentWarning: String?,
|
||||||
|
sensitive: Boolean,
|
||||||
|
visibility: Status.Visibility,
|
||||||
|
mediaUris: List<String>,
|
||||||
|
mediaDescriptions: List<String?>,
|
||||||
|
poll: NewPoll?,
|
||||||
|
failedToSend: Boolean
|
||||||
|
): Completable {
|
||||||
|
return Single.fromCallable {
|
||||||
|
|
||||||
|
val draftDirectory = context.getExternalFilesDir("Tusky")
|
||||||
|
|
||||||
|
if (draftDirectory == null || !(draftDirectory.exists())) {
|
||||||
|
Log.e("DraftHelper", "Error obtaining directory to save media.")
|
||||||
|
throw Exception()
|
||||||
|
}
|
||||||
|
|
||||||
|
val uris = mediaUris.map { uriString ->
|
||||||
|
uriString.toUri()
|
||||||
|
}.map { uri ->
|
||||||
|
if (uri.isNotInFolder(draftDirectory)) {
|
||||||
|
uri.copyToFolder(draftDirectory)
|
||||||
|
} else {
|
||||||
|
uri
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val types = uris.map { uri ->
|
||||||
|
val mimeType = context.contentResolver.getType(uri)
|
||||||
|
when (mimeType?.substring(0, mimeType.indexOf('/'))) {
|
||||||
|
"video" -> DraftAttachment.Type.VIDEO
|
||||||
|
"image" -> DraftAttachment.Type.IMAGE
|
||||||
|
"audio" -> DraftAttachment.Type.AUDIO
|
||||||
|
else -> throw IllegalStateException("unknown media type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val attachments: MutableList<DraftAttachment> = mutableListOf()
|
||||||
|
for (i in mediaUris.indices) {
|
||||||
|
attachments.add(
|
||||||
|
DraftAttachment(
|
||||||
|
uriString = uris[i].toString(),
|
||||||
|
description = mediaDescriptions[i],
|
||||||
|
type = types[i]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
DraftEntity(
|
||||||
|
id = draftId,
|
||||||
|
accountId = accountId,
|
||||||
|
inReplyToId = inReplyToId,
|
||||||
|
content = content,
|
||||||
|
contentWarning = contentWarning,
|
||||||
|
sensitive = sensitive,
|
||||||
|
visibility = visibility,
|
||||||
|
attachments = attachments,
|
||||||
|
poll = poll,
|
||||||
|
failedToSend = failedToSend
|
||||||
|
)
|
||||||
|
|
||||||
|
}.flatMapCompletable { draft ->
|
||||||
|
draftDao.insertOrReplace(draft)
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteDraftAndAttachments(draftId: Int): Completable {
|
||||||
|
return draftDao.find(draftId)
|
||||||
|
.flatMapCompletable { draft ->
|
||||||
|
deleteDraftAndAttachments(draft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteDraftAndAttachments(draft: DraftEntity): Completable {
|
||||||
|
return deleteAttachments(draft)
|
||||||
|
.andThen(draftDao.delete(draft.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteAttachments(draft: DraftEntity): Completable {
|
||||||
|
return Completable.fromCallable {
|
||||||
|
draft.attachments.forEach { attachment ->
|
||||||
|
if (context.contentResolver.delete(attachment.uri, null, null) == 0) {
|
||||||
|
Log.e("DraftHelper", "Did not delete file ${attachment.uriString}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.subscribeOn(Schedulers.io())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Uri.isNotInFolder(folder: File): Boolean {
|
||||||
|
val filePath = path ?: return true
|
||||||
|
return File(filePath).parentFile == folder
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Uri.copyToFolder(folder: File): Uri {
|
||||||
|
val contentResolver = context.contentResolver
|
||||||
|
|
||||||
|
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date())
|
||||||
|
|
||||||
|
val mimeType = contentResolver.getType(this)
|
||||||
|
val map = MimeTypeMap.getSingleton()
|
||||||
|
val fileExtension = map.getExtensionFromMimeType(mimeType)
|
||||||
|
|
||||||
|
val filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension)
|
||||||
|
val file = File(folder, filename)
|
||||||
|
IOUtils.copyToFile(contentResolver, this, file)
|
||||||
|
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,81 @@
|
||||||
|
/* Copyright 2020 Tusky Contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.components.drafts
|
||||||
|
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.appcompat.widget.AppCompatImageView
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListAdapter
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.db.DraftAttachment
|
||||||
|
|
||||||
|
class DraftMediaAdapter(
|
||||||
|
private val attachmentClick: () -> Unit
|
||||||
|
) : ListAdapter<DraftAttachment, DraftMediaAdapter.DraftMediaViewHolder>(
|
||||||
|
object: DiffUtil.ItemCallback<DraftAttachment>() {
|
||||||
|
override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DraftMediaViewHolder {
|
||||||
|
return DraftMediaViewHolder(AppCompatImageView(parent.context))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: DraftMediaViewHolder, position: Int) {
|
||||||
|
getItem(position)?.let { attachment ->
|
||||||
|
if (attachment.type == DraftAttachment.Type.AUDIO) {
|
||||||
|
holder.imageView.setImageResource(R.drawable.ic_music_box_preview_24dp)
|
||||||
|
} else {
|
||||||
|
Glide.with(holder.itemView.context)
|
||||||
|
.load(attachment.uri)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.dontAnimate()
|
||||||
|
.into(holder.imageView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class DraftMediaViewHolder(val imageView: ImageView)
|
||||||
|
: RecyclerView.ViewHolder(imageView) {
|
||||||
|
init {
|
||||||
|
val thumbnailViewSize =
|
||||||
|
imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size)
|
||||||
|
val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize)
|
||||||
|
val margin = itemView.context.resources
|
||||||
|
.getDimensionPixelSize(R.dimen.compose_media_preview_margin)
|
||||||
|
val marginBottom = itemView.context.resources
|
||||||
|
.getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom)
|
||||||
|
layoutParams.setMargins(margin, 0, margin, marginBottom)
|
||||||
|
imageView.layoutParams = layoutParams
|
||||||
|
imageView.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||||
|
imageView.setOnClickListener {
|
||||||
|
attachmentClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,197 @@
|
||||||
|
/* Copyright 2020 Tusky Contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.components.drafts
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.keylesspalace.tusky.BaseActivity
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.SavedTootActivity
|
||||||
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
|
import com.keylesspalace.tusky.databinding.ActivityDraftsBinding
|
||||||
|
import com.keylesspalace.tusky.db.DraftEntity
|
||||||
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.uber.autodispose.android.lifecycle.autoDispose
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import retrofit2.HttpException
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DraftsActivity : BaseActivity(), DraftActionListener {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var viewModelFactory: ViewModelFactory
|
||||||
|
|
||||||
|
private val viewModel: DraftsViewModel by viewModels { viewModelFactory }
|
||||||
|
|
||||||
|
private lateinit var binding: ActivityDraftsBinding
|
||||||
|
private lateinit var bottomSheet: BottomSheetBehavior<LinearLayout>
|
||||||
|
|
||||||
|
private var oldDraftsButton: MenuItem? = null
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
binding = ActivityDraftsBinding.inflate(layoutInflater)
|
||||||
|
setContentView(binding.root)
|
||||||
|
|
||||||
|
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||||
|
supportActionBar?.apply {
|
||||||
|
title = getString(R.string.title_drafts)
|
||||||
|
setDisplayHomeAsUpEnabled(true)
|
||||||
|
setDisplayShowHomeEnabled(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
binding.draftsErrorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_saved_status)
|
||||||
|
|
||||||
|
val adapter = DraftsAdapter(this)
|
||||||
|
|
||||||
|
binding.draftsRecyclerView.adapter = adapter
|
||||||
|
binding.draftsRecyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
|
binding.draftsRecyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
|
||||||
|
|
||||||
|
bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root)
|
||||||
|
|
||||||
|
viewModel.drafts.observe(this) { draftList ->
|
||||||
|
if (draftList.isEmpty()) {
|
||||||
|
binding.draftsRecyclerView.hide()
|
||||||
|
binding.draftsErrorMessageView.show()
|
||||||
|
} else {
|
||||||
|
binding.draftsRecyclerView.show()
|
||||||
|
binding.draftsErrorMessageView.hide()
|
||||||
|
adapter.submitList(draftList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
menuInflater.inflate(R.menu.drafts, menu)
|
||||||
|
oldDraftsButton = menu.findItem(R.id.action_old_drafts)
|
||||||
|
viewModel.showOldDraftsButton()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.autoDispose(this, Lifecycle.Event.ON_DESTROY)
|
||||||
|
.subscribe { showOldDraftsButton ->
|
||||||
|
oldDraftsButton?.isVisible = showOldDraftsButton
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
android.R.id.home -> {
|
||||||
|
onBackPressed()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
R.id.action_old_drafts -> {
|
||||||
|
val intent = Intent(this, SavedTootActivity::class.java)
|
||||||
|
startActivityWithSlideInAnimation(intent)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOpenDraft(draft: DraftEntity) {
|
||||||
|
|
||||||
|
if (draft.inReplyToId != null) {
|
||||||
|
bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
viewModel.getToot(draft.inReplyToId)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.autoDispose(this)
|
||||||
|
.subscribe({ status ->
|
||||||
|
val composeOptions = ComposeActivity.ComposeOptions(
|
||||||
|
draftId = draft.id,
|
||||||
|
tootText = draft.content,
|
||||||
|
contentWarning = draft.contentWarning,
|
||||||
|
inReplyToId = draft.inReplyToId,
|
||||||
|
replyingStatusContent = status.content.toString(),
|
||||||
|
replyingStatusAuthor = status.account.localUsername,
|
||||||
|
draftAttachments = draft.attachments,
|
||||||
|
poll = draft.poll,
|
||||||
|
sensitive = draft.sensitive,
|
||||||
|
visibility = draft.visibility
|
||||||
|
)
|
||||||
|
|
||||||
|
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
|
||||||
|
startActivity(ComposeActivity.startIntent(this, composeOptions))
|
||||||
|
|
||||||
|
}, { throwable ->
|
||||||
|
|
||||||
|
bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
|
||||||
|
Log.w(TAG, "failed loading reply information", throwable)
|
||||||
|
|
||||||
|
if (throwable is HttpException && throwable.code() == 404) {
|
||||||
|
// the original status to which a reply was drafted has been deleted
|
||||||
|
// let's open the ComposeActivity without reply information
|
||||||
|
Toast.makeText(this, getString(R.string.drafts_toot_reply_removed), Toast.LENGTH_LONG).show()
|
||||||
|
openDraftWithoutReply(draft)
|
||||||
|
} else {
|
||||||
|
Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
openDraftWithoutReply(draft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openDraftWithoutReply(draft: DraftEntity) {
|
||||||
|
val composeOptions = ComposeActivity.ComposeOptions(
|
||||||
|
draftId = draft.id,
|
||||||
|
tootText = draft.content,
|
||||||
|
contentWarning = draft.contentWarning,
|
||||||
|
draftAttachments = draft.attachments,
|
||||||
|
poll = draft.poll,
|
||||||
|
sensitive = draft.sensitive,
|
||||||
|
visibility = draft.visibility
|
||||||
|
)
|
||||||
|
|
||||||
|
startActivity(ComposeActivity.startIntent(this, composeOptions))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDeleteDraft(draft: DraftEntity) {
|
||||||
|
viewModel.deleteDraft(draft)
|
||||||
|
Snackbar.make(binding.root, getString(R.string.draft_deleted), Snackbar.LENGTH_LONG)
|
||||||
|
.setAction(R.string.action_undo) {
|
||||||
|
viewModel.restoreDraft(draft)
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "DraftsActivity"
|
||||||
|
|
||||||
|
fun newIntent(context: Context) = Intent(context, DraftsActivity::class.java)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
/* Copyright 2021 Tusky Contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.components.drafts
|
||||||
|
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.paging.PagedListAdapter
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import com.keylesspalace.tusky.databinding.ItemDraftBinding
|
||||||
|
import com.keylesspalace.tusky.db.DraftEntity
|
||||||
|
import com.keylesspalace.tusky.util.BindingViewHolder
|
||||||
|
import com.keylesspalace.tusky.util.hide
|
||||||
|
import com.keylesspalace.tusky.util.show
|
||||||
|
import com.keylesspalace.tusky.util.visible
|
||||||
|
|
||||||
|
interface DraftActionListener {
|
||||||
|
fun onOpenDraft(draft: DraftEntity)
|
||||||
|
fun onDeleteDraft(draft: DraftEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
class DraftsAdapter(
|
||||||
|
private val listener: DraftActionListener
|
||||||
|
) : PagedListAdapter<DraftEntity, BindingViewHolder<ItemDraftBinding>>(
|
||||||
|
object : DiffUtil.ItemCallback<DraftEntity>() {
|
||||||
|
override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
|
||||||
|
return oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean {
|
||||||
|
return oldItem == newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder<ItemDraftBinding> {
|
||||||
|
|
||||||
|
val binding = ItemDraftBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||||
|
|
||||||
|
val viewHolder = BindingViewHolder(binding)
|
||||||
|
|
||||||
|
binding.draftMediaPreview.layoutManager = LinearLayoutManager(binding.root.context, RecyclerView.HORIZONTAL, false)
|
||||||
|
binding.draftMediaPreview.adapter = DraftMediaAdapter {
|
||||||
|
getItem(viewHolder.adapterPosition)?.let { draft ->
|
||||||
|
listener.onOpenDraft(draft)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return viewHolder
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: BindingViewHolder<ItemDraftBinding>, position: Int) {
|
||||||
|
getItem(position)?.let { draft ->
|
||||||
|
holder.binding.root.setOnClickListener {
|
||||||
|
listener.onOpenDraft(draft)
|
||||||
|
}
|
||||||
|
holder.binding.deleteButton.setOnClickListener {
|
||||||
|
listener.onDeleteDraft(draft)
|
||||||
|
}
|
||||||
|
holder.binding.draftSendingInfo.visible(draft.failedToSend)
|
||||||
|
|
||||||
|
holder.binding.contentWarning.visible(!draft.contentWarning.isNullOrEmpty())
|
||||||
|
holder.binding.contentWarning.text = draft.contentWarning
|
||||||
|
holder.binding.content.text = draft.content
|
||||||
|
|
||||||
|
holder.binding.draftMediaPreview.visible(draft.attachments.isNotEmpty())
|
||||||
|
(holder.binding.draftMediaPreview.adapter as DraftMediaAdapter).submitList(draft.attachments)
|
||||||
|
|
||||||
|
if (draft.poll != null) {
|
||||||
|
holder.binding.draftPoll.show()
|
||||||
|
holder.binding.draftPoll.setPoll(draft.poll)
|
||||||
|
} else {
|
||||||
|
holder.binding.draftPoll.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
/* Copyright 2020 Tusky Contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.components.drafts
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.paging.toLiveData
|
||||||
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
|
import com.keylesspalace.tusky.db.DraftEntity
|
||||||
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.Single
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DraftsViewModel @Inject constructor(
|
||||||
|
val database: AppDatabase,
|
||||||
|
val accountManager: AccountManager,
|
||||||
|
val api: MastodonApi,
|
||||||
|
val draftHelper: DraftHelper
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
|
val drafts = database.draftDao().loadDrafts(accountManager.activeAccount?.id!!).toLiveData(pageSize = 20)
|
||||||
|
|
||||||
|
private val deletedDrafts: MutableList<DraftEntity> = mutableListOf()
|
||||||
|
|
||||||
|
fun showOldDraftsButton(): Observable<Boolean> {
|
||||||
|
return database.tootDao().savedTootCount()
|
||||||
|
.map { count -> count > 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteDraft(draft: DraftEntity) {
|
||||||
|
// this does not immediately delete media files to avoid unnecessary file operations
|
||||||
|
// in case the user decides to restore the draft
|
||||||
|
database.draftDao().delete(draft.id)
|
||||||
|
.subscribe()
|
||||||
|
deletedDrafts.add(draft)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreDraft(draft: DraftEntity) {
|
||||||
|
database.draftDao().insertOrReplace(draft)
|
||||||
|
.subscribe()
|
||||||
|
deletedDrafts.remove(draft)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getToot(tootId: String): Single<Status> {
|
||||||
|
return api.statusSingle(tootId)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
deletedDrafts.forEach {
|
||||||
|
draftHelper.deleteAttachments(it).subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -120,7 +120,7 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec
|
||||||
|
|
||||||
override fun edit(item: ScheduledStatus) {
|
override fun edit(item: ScheduledStatus) {
|
||||||
val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions(
|
val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions(
|
||||||
scheduledTootUid = item.id,
|
scheduledTootId = item.id,
|
||||||
tootText = item.params.text,
|
tootText = item.params.text,
|
||||||
contentWarning = item.params.spoilerText,
|
contentWarning = item.params.spoilerText,
|
||||||
mediaAttachments = item.mediaAttachments,
|
mediaAttachments = item.mediaAttachments,
|
||||||
|
|
|
@ -28,9 +28,9 @@ import com.keylesspalace.tusky.components.conversation.ConversationEntity;
|
||||||
* DB version & declare DAO
|
* DB version & declare DAO
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
@Database(entities = { TootEntity.class, DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
|
||||||
TimelineAccountEntity.class, ConversationEntity.class
|
TimelineAccountEntity.class, ConversationEntity.class
|
||||||
}, version = 24)
|
}, version = 25)
|
||||||
public abstract class AppDatabase extends RoomDatabase {
|
public abstract class AppDatabase extends RoomDatabase {
|
||||||
|
|
||||||
public abstract TootDao tootDao();
|
public abstract TootDao tootDao();
|
||||||
|
@ -38,6 +38,7 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
public abstract InstanceDao instanceDao();
|
public abstract InstanceDao instanceDao();
|
||||||
public abstract ConversationsDao conversationDao();
|
public abstract ConversationsDao conversationDao();
|
||||||
public abstract TimelineDao timelineDao();
|
public abstract TimelineDao timelineDao();
|
||||||
|
public abstract DraftDao draftDao();
|
||||||
|
|
||||||
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
|
public static final Migration MIGRATION_2_3 = new Migration(2, 3) {
|
||||||
@Override
|
@Override
|
||||||
|
@ -46,7 +47,6 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
database.execSQL("INSERT INTO TootEntity2 SELECT * FROM TootEntity;");
|
database.execSQL("INSERT INTO TootEntity2 SELECT * FROM TootEntity;");
|
||||||
database.execSQL("DROP TABLE TootEntity;");
|
database.execSQL("DROP TABLE TootEntity;");
|
||||||
database.execSQL("ALTER TABLE TootEntity2 RENAME TO TootEntity;");
|
database.execSQL("ALTER TABLE TootEntity2 RENAME TO TootEntity;");
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -347,4 +347,22 @@ public abstract class AppDatabase extends RoomDatabase {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static final Migration MIGRATION_24_25 = new Migration(24, 25) {
|
||||||
|
@Override
|
||||||
|
public void migrate(@NonNull SupportSQLiteDatabase database) {
|
||||||
|
database.execSQL(
|
||||||
|
"CREATE TABLE IF NOT EXISTS `DraftEntity` (" +
|
||||||
|
"`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " +
|
||||||
|
"`accountId` INTEGER NOT NULL, " +
|
||||||
|
"`inReplyToId` TEXT," +
|
||||||
|
"`content` TEXT," +
|
||||||
|
"`contentWarning` TEXT," +
|
||||||
|
"`sensitive` INTEGER NOT NULL," +
|
||||||
|
"`visibility` INTEGER NOT NULL," +
|
||||||
|
"`attachments` TEXT NOT NULL," +
|
||||||
|
"`poll` TEXT," +
|
||||||
|
"`failedToSend` INTEGER NOT NULL)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,10 +24,7 @@ import com.google.gson.reflect.TypeToken
|
||||||
import com.keylesspalace.tusky.TabData
|
import com.keylesspalace.tusky.TabData
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
|
import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity
|
||||||
import com.keylesspalace.tusky.createTabDataFromId
|
import com.keylesspalace.tusky.createTabDataFromId
|
||||||
import com.keylesspalace.tusky.entity.Attachment
|
import com.keylesspalace.tusky.entity.*
|
||||||
import com.keylesspalace.tusky.entity.Emoji
|
|
||||||
import com.keylesspalace.tusky.entity.Poll
|
|
||||||
import com.keylesspalace.tusky.entity.Status
|
|
||||||
import com.keylesspalace.tusky.json.SpannedTypeAdapter
|
import com.keylesspalace.tusky.json.SpannedTypeAdapter
|
||||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
||||||
import java.net.URLDecoder
|
import java.net.URLDecoder
|
||||||
|
@ -151,4 +148,23 @@ class Converters {
|
||||||
return gson.fromJson(pollJson, Poll::class.java)
|
return gson.fromJson(pollJson, Poll::class.java)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
@TypeConverter
|
||||||
|
fun newPollToJson(newPoll: NewPoll?): String? {
|
||||||
|
return gson.toJson(newPoll)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun jsonToNewPoll(newPollJson: String?): NewPoll? {
|
||||||
|
return gson.fromJson(newPollJson, NewPoll::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun draftAttachmentListToJson(draftAttachments: List<DraftAttachment>?): String? {
|
||||||
|
return gson.toJson(draftAttachments)
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun jsonToDraftAttachmentList(draftAttachmentListJson: String?): List<DraftAttachment>? {
|
||||||
|
return gson.fromJson(draftAttachmentListJson, object : TypeToken<List<DraftAttachment>>() {}.type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
40
app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt
Normal file
40
app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
/* Copyright 2020 Tusky Contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.db
|
||||||
|
|
||||||
|
import androidx.paging.DataSource
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import io.reactivex.Completable
|
||||||
|
import io.reactivex.Single
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface DraftDao {
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insertOrReplace(draft: DraftEntity): Completable
|
||||||
|
|
||||||
|
@Query("SELECT * FROM DraftEntity WHERE accountId = :accountId ORDER BY id ASC")
|
||||||
|
fun loadDrafts(accountId: Long): DataSource.Factory<Int, DraftEntity>
|
||||||
|
|
||||||
|
@Query("DELETE FROM DraftEntity WHERE id = :id")
|
||||||
|
fun delete(id: Int): Completable
|
||||||
|
|
||||||
|
@Query("SELECT * FROM DraftEntity WHERE id = :id")
|
||||||
|
fun find(id: Int): Single<DraftEntity?>
|
||||||
|
}
|
55
app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt
Normal file
55
app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
/* Copyright 2020 Tusky Contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
package com.keylesspalace.tusky.db
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import com.keylesspalace.tusky.entity.NewPoll
|
||||||
|
import com.keylesspalace.tusky.entity.Status
|
||||||
|
import kotlinx.android.parcel.Parcelize
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
|
data class DraftEntity(
|
||||||
|
@PrimaryKey(autoGenerate = true) val id: Int = 0,
|
||||||
|
val accountId: Long,
|
||||||
|
val inReplyToId: String?,
|
||||||
|
val content: String?,
|
||||||
|
val contentWarning: String?,
|
||||||
|
val sensitive: Boolean,
|
||||||
|
val visibility: Status.Visibility,
|
||||||
|
val attachments: List<DraftAttachment>,
|
||||||
|
val poll: NewPoll?,
|
||||||
|
val failedToSend: Boolean
|
||||||
|
)
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
data class DraftAttachment(
|
||||||
|
val uriString: String,
|
||||||
|
val description: String?,
|
||||||
|
val type: Type
|
||||||
|
): Parcelable {
|
||||||
|
val uri: Uri
|
||||||
|
get() = uriString.toUri()
|
||||||
|
|
||||||
|
enum class Type {
|
||||||
|
IMAGE, VIDEO, AUDIO;
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,7 +26,7 @@ import com.keylesspalace.tusky.entity.Status
|
||||||
// Avoiding rescanning status table when accounts table changes. Recommended by Room(c).
|
// Avoiding rescanning status table when accounts table changes. Recommended by Room(c).
|
||||||
indices = [Index("authorServerId", "timelineUserId")]
|
indices = [Index("authorServerId", "timelineUserId")]
|
||||||
)
|
)
|
||||||
@TypeConverters(TootEntity.Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
data class TimelineStatusEntity(
|
data class TimelineStatusEntity(
|
||||||
val serverId: String, // id never flips: we need it for sorting so it's a real id
|
val serverId: String, // id never flips: we need it for sorting so it's a real id
|
||||||
val url: String?,
|
val url: String?,
|
||||||
|
|
|
@ -16,12 +16,12 @@
|
||||||
package com.keylesspalace.tusky.db;
|
package com.keylesspalace.tusky.db;
|
||||||
|
|
||||||
import androidx.room.Dao;
|
import androidx.room.Dao;
|
||||||
import androidx.room.Insert;
|
|
||||||
import androidx.room.OnConflictStrategy;
|
|
||||||
import androidx.room.Query;
|
import androidx.room.Query;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by cto3543 on 28/06/2017.
|
* Created by cto3543 on 28/06/2017.
|
||||||
*
|
*
|
||||||
|
@ -30,8 +30,6 @@ import java.util.List;
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
public interface TootDao {
|
public interface TootDao {
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
void insertOrReplace(TootEntity users);
|
|
||||||
|
|
||||||
@Query("SELECT * FROM TootEntity ORDER BY uid DESC")
|
@Query("SELECT * FROM TootEntity ORDER BY uid DESC")
|
||||||
List<TootEntity> loadAll();
|
List<TootEntity> loadAll();
|
||||||
|
@ -41,4 +39,7 @@ public interface TootDao {
|
||||||
|
|
||||||
@Query("SELECT * FROM TootEntity WHERE uid = :uid")
|
@Query("SELECT * FROM TootEntity WHERE uid = :uid")
|
||||||
TootEntity find(int uid);
|
TootEntity find(int uid);
|
||||||
}
|
|
||||||
|
@Query("SELECT COUNT(*) FROM TootEntity")
|
||||||
|
Observable<Integer> savedTootCount();
|
||||||
|
}
|
|
@ -18,6 +18,7 @@ package com.keylesspalace.tusky.di
|
||||||
import com.keylesspalace.tusky.*
|
import com.keylesspalace.tusky.*
|
||||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
|
import com.keylesspalace.tusky.components.drafts.DraftsActivity
|
||||||
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
|
||||||
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
import com.keylesspalace.tusky.components.preference.PreferencesActivity
|
||||||
import com.keylesspalace.tusky.components.report.ReportActivity
|
import com.keylesspalace.tusky.components.report.ReportActivity
|
||||||
|
@ -107,4 +108,7 @@ abstract class ActivitiesModule {
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity
|
abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract fun contributesDraftActivity(): DraftsActivity
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ class AppModule {
|
||||||
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
|
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
|
||||||
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
|
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
|
||||||
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
|
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
|
||||||
AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24)
|
AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider
|
||||||
import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel
|
import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||||
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
import com.keylesspalace.tusky.components.conversation.ConversationsViewModel
|
||||||
|
import com.keylesspalace.tusky.components.drafts.DraftsViewModel
|
||||||
import com.keylesspalace.tusky.components.report.ReportViewModel
|
import com.keylesspalace.tusky.components.report.ReportViewModel
|
||||||
import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel
|
import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel
|
||||||
import com.keylesspalace.tusky.components.search.SearchViewModel
|
import com.keylesspalace.tusky.components.search.SearchViewModel
|
||||||
|
@ -91,5 +92,10 @@ abstract class ViewModelModule {
|
||||||
@ViewModelKey(AnnouncementsViewModel::class)
|
@ViewModelKey(AnnouncementsViewModel::class)
|
||||||
internal abstract fun announcementsViewModel(viewModel: AnnouncementsViewModel): ViewModel
|
internal abstract fun announcementsViewModel(viewModel: AnnouncementsViewModel): ViewModel
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
@IntoMap
|
||||||
|
@ViewModelKey(DraftsViewModel::class)
|
||||||
|
internal abstract fun draftsViewModel(viewModel: DraftsViewModel): ViewModel
|
||||||
|
|
||||||
//Add more ViewModels here
|
//Add more ViewModels here
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,7 +124,8 @@ interface MastodonApi {
|
||||||
@Multipart
|
@Multipart
|
||||||
@POST("api/v1/media")
|
@POST("api/v1/media")
|
||||||
fun uploadMedia(
|
fun uploadMedia(
|
||||||
@Part file: MultipartBody.Part
|
@Part file: MultipartBody.Part,
|
||||||
|
@Part description: MultipartBody.Part? = null
|
||||||
): Single<Attachment>
|
): Single<Attachment>
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
|
@ -147,6 +148,11 @@ interface MastodonApi {
|
||||||
@Path("id") statusId: String
|
@Path("id") statusId: String
|
||||||
): Call<Status>
|
): Call<Status>
|
||||||
|
|
||||||
|
@GET("api/v1/statuses/{id}")
|
||||||
|
fun statusSingle(
|
||||||
|
@Path("id") statusId: String
|
||||||
|
): Single<Status>
|
||||||
|
|
||||||
@GET("api/v1/statuses/{id}/context")
|
@GET("api/v1/statuses/{id}/context")
|
||||||
fun statusContext(
|
fun statusContext(
|
||||||
@Path("id") statusId: String
|
@Path("id") statusId: String
|
||||||
|
|
|
@ -60,7 +60,6 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
||||||
|
|
||||||
val notificationManager = NotificationManagerCompat.from(context)
|
val notificationManager = NotificationManagerCompat.from(context)
|
||||||
|
|
||||||
|
|
||||||
if (intent.action == NotificationHelper.REPLY_ACTION) {
|
if (intent.action == NotificationHelper.REPLY_ACTION) {
|
||||||
|
|
||||||
val message = getReplyMessage(intent)
|
val message = getReplyMessage(intent)
|
||||||
|
@ -89,22 +88,23 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() {
|
||||||
val sendIntent = SendTootService.sendTootIntent(
|
val sendIntent = SendTootService.sendTootIntent(
|
||||||
context,
|
context,
|
||||||
TootToSend(
|
TootToSend(
|
||||||
text,
|
text = text,
|
||||||
spoiler,
|
warningText = spoiler,
|
||||||
visibility.serverString(),
|
visibility = visibility.serverString(),
|
||||||
false,
|
sensitive = false,
|
||||||
emptyList(),
|
mediaIds = emptyList(),
|
||||||
emptyList(),
|
mediaUris = emptyList(),
|
||||||
emptyList(),
|
mediaDescriptions = emptyList(),
|
||||||
null,
|
scheduledAt = null,
|
||||||
citedStatusId,
|
inReplyToId = citedStatusId,
|
||||||
null,
|
poll = null,
|
||||||
null,
|
replyingStatusContent = null,
|
||||||
null,
|
replyingStatusAuthorUsername = null,
|
||||||
null, account.id,
|
accountId = account.id,
|
||||||
0,
|
savedTootUid = -1,
|
||||||
randomAlphanumericString(16),
|
draftId = -1,
|
||||||
0
|
idempotencyKey = randomAlphanumericString(16),
|
||||||
|
retries = 0
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import com.keylesspalace.tusky.R
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
import com.keylesspalace.tusky.appstore.StatusComposedEvent
|
||||||
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
import com.keylesspalace.tusky.appstore.StatusScheduledEvent
|
||||||
|
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||||
import com.keylesspalace.tusky.db.AccountManager
|
import com.keylesspalace.tusky.db.AccountManager
|
||||||
import com.keylesspalace.tusky.db.AppDatabase
|
import com.keylesspalace.tusky.db.AppDatabase
|
||||||
import com.keylesspalace.tusky.di.Injectable
|
import com.keylesspalace.tusky.di.Injectable
|
||||||
|
@ -46,7 +47,8 @@ class SendTootService : Service(), Injectable {
|
||||||
lateinit var eventHub: EventHub
|
lateinit var eventHub: EventHub
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var database: AppDatabase
|
lateinit var database: AppDatabase
|
||||||
|
@Inject
|
||||||
|
lateinit var draftHelper: DraftHelper
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var saveTootHelper: SaveTootHelper
|
lateinit var saveTootHelper: SaveTootHelper
|
||||||
|
|
||||||
|
@ -163,6 +165,10 @@ class SendTootService : Service(), Injectable {
|
||||||
if (tootToSend.savedTootUid != 0) {
|
if (tootToSend.savedTootUid != 0) {
|
||||||
saveTootHelper.deleteDraft(tootToSend.savedTootUid)
|
saveTootHelper.deleteDraft(tootToSend.savedTootUid)
|
||||||
}
|
}
|
||||||
|
if (tootToSend.draftId != 0) {
|
||||||
|
draftHelper.deleteDraftAndAttachments(tootToSend.draftId)
|
||||||
|
.subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
if (scheduled) {
|
if (scheduled) {
|
||||||
response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch)
|
response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch)
|
||||||
|
@ -245,17 +251,19 @@ class SendTootService : Service(), Injectable {
|
||||||
|
|
||||||
private fun saveTootToDrafts(toot: TootToSend) {
|
private fun saveTootToDrafts(toot: TootToSend) {
|
||||||
|
|
||||||
saveTootHelper.saveToot(toot.text,
|
draftHelper.saveDraft(
|
||||||
toot.warningText,
|
draftId = toot.draftId,
|
||||||
toot.savedJsonUrls,
|
accountId = toot.accountId,
|
||||||
toot.mediaUris,
|
inReplyToId = toot.inReplyToId,
|
||||||
toot.mediaDescriptions,
|
content = toot.text,
|
||||||
toot.savedTootUid,
|
contentWarning = toot.warningText,
|
||||||
toot.inReplyToId,
|
sensitive = toot.sensitive,
|
||||||
toot.replyingStatusContent,
|
visibility = Status.Visibility.byString(toot.visibility),
|
||||||
toot.replyingStatusAuthorUsername,
|
mediaUris = toot.mediaUris,
|
||||||
Status.Visibility.byString(toot.visibility),
|
mediaDescriptions = toot.mediaDescriptions,
|
||||||
toot.poll)
|
poll = toot.poll,
|
||||||
|
failedToSend = true
|
||||||
|
).subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun cancelSendingIntent(tootId: Int): PendingIntent {
|
private fun cancelSendingIntent(tootId: Int): PendingIntent {
|
||||||
|
@ -323,9 +331,9 @@ data class TootToSend(
|
||||||
val poll: NewPoll?,
|
val poll: NewPoll?,
|
||||||
val replyingStatusContent: String?,
|
val replyingStatusContent: String?,
|
||||||
val replyingStatusAuthorUsername: String?,
|
val replyingStatusAuthorUsername: String?,
|
||||||
val savedJsonUrls: List<String>?,
|
|
||||||
val accountId: Long,
|
val accountId: Long,
|
||||||
val savedTootUid: Int,
|
val savedTootUid: Int,
|
||||||
|
val draftId: Int,
|
||||||
val idempotencyKey: String,
|
val idempotencyKey: String,
|
||||||
var retries: Int
|
var retries: Int
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
package com.keylesspalace.tusky.util
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
|
||||||
|
class BindingViewHolder<T : ViewBinding>(
|
||||||
|
val binding: T
|
||||||
|
) : RecyclerView.ViewHolder(binding.root)
|
|
@ -1,5 +1,6 @@
|
||||||
package com.keylesspalace.tusky.util
|
package com.keylesspalace.tusky.util
|
||||||
|
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.disposables.Disposable
|
||||||
|
@ -9,6 +10,7 @@ open class RxAwareViewModel : ViewModel() {
|
||||||
|
|
||||||
fun Disposable.autoDispose() = disposables.add(this)
|
fun Disposable.autoDispose() = disposables.add(this)
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
disposables.clear()
|
disposables.clear()
|
||||||
|
|
|
@ -1,33 +1,18 @@
|
||||||
package com.keylesspalace.tusky.util;
|
package com.keylesspalace.tusky.util;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.webkit.MimeTypeMap;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.content.FileProvider;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
import com.keylesspalace.tusky.BuildConfig;
|
|
||||||
import com.keylesspalace.tusky.db.AppDatabase;
|
import com.keylesspalace.tusky.db.AppDatabase;
|
||||||
import com.keylesspalace.tusky.db.TootDao;
|
import com.keylesspalace.tusky.db.TootDao;
|
||||||
import com.keylesspalace.tusky.db.TootEntity;
|
import com.keylesspalace.tusky.db.TootEntity;
|
||||||
import com.keylesspalace.tusky.entity.NewPoll;
|
|
||||||
import com.keylesspalace.tusky.entity.Status;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
@ -45,61 +30,6 @@ public final class SaveTootHelper {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
public boolean saveToot(@NonNull String content,
|
|
||||||
@NonNull String contentWarning,
|
|
||||||
@Nullable List<String> savedJsonUrls,
|
|
||||||
@NonNull List<String> mediaUris,
|
|
||||||
@NonNull List<String> mediaDescriptions,
|
|
||||||
int savedTootUid,
|
|
||||||
@Nullable String inReplyToId,
|
|
||||||
@Nullable String replyingStatusContent,
|
|
||||||
@Nullable String replyingStatusAuthorUsername,
|
|
||||||
@NonNull Status.Visibility statusVisibility,
|
|
||||||
@Nullable NewPoll poll) {
|
|
||||||
|
|
||||||
if (TextUtils.isEmpty(content) && mediaUris.isEmpty() && poll == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get any existing file's URIs.
|
|
||||||
|
|
||||||
String mediaUrlsSerialized = null;
|
|
||||||
String mediaDescriptionsSerialized = null;
|
|
||||||
|
|
||||||
if (!ListUtils.isEmpty(mediaUris)) {
|
|
||||||
List<String> savedList = saveMedia(mediaUris, savedJsonUrls);
|
|
||||||
if (!ListUtils.isEmpty(savedList)) {
|
|
||||||
mediaUrlsSerialized = gson.toJson(savedList);
|
|
||||||
if (!ListUtils.isEmpty(savedJsonUrls)) {
|
|
||||||
deleteMedia(setDifference(savedJsonUrls, savedList));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
mediaDescriptionsSerialized = gson.toJson(mediaDescriptions);
|
|
||||||
} else if (!ListUtils.isEmpty(savedJsonUrls)) {
|
|
||||||
/* If there were URIs in the previous draft, but they've now been removed, those files
|
|
||||||
* can be deleted. */
|
|
||||||
deleteMedia(savedJsonUrls);
|
|
||||||
}
|
|
||||||
final TootEntity toot = new TootEntity(savedTootUid, content, mediaUrlsSerialized, mediaDescriptionsSerialized, contentWarning,
|
|
||||||
inReplyToId,
|
|
||||||
replyingStatusContent,
|
|
||||||
replyingStatusAuthorUsername,
|
|
||||||
statusVisibility,
|
|
||||||
poll);
|
|
||||||
|
|
||||||
new AsyncTask<Void, Void, Void>() {
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... params) {
|
|
||||||
tootDao.insertOrReplace(toot);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}.execute();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void deleteDraft(int tootId) {
|
public void deleteDraft(int tootId) {
|
||||||
TootEntity item = tootDao.find(tootId);
|
TootEntity item = tootDao.find(tootId);
|
||||||
if (item != null) {
|
if (item != null) {
|
||||||
|
@ -124,82 +54,4 @@ public final class SaveTootHelper {
|
||||||
tootDao.delete(item.getUid());
|
tootDao.delete(item.getUid());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
}
|
||||||
private List<String> saveMedia(@NonNull List<String> mediaUris,
|
|
||||||
@Nullable List<String> existingUris) {
|
|
||||||
|
|
||||||
File directory = context.getExternalFilesDir("Tusky");
|
|
||||||
|
|
||||||
if (directory == null || !(directory.exists())) {
|
|
||||||
Log.e(TAG, "Error obtaining directory to save media.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
ContentResolver contentResolver = context.getContentResolver();
|
|
||||||
ArrayList<File> filesSoFar = new ArrayList<>();
|
|
||||||
ArrayList<String> results = new ArrayList<>();
|
|
||||||
for (String mediaUri : mediaUris) {
|
|
||||||
/* If the media was already saved in a previous draft, there's no need to save another
|
|
||||||
* copy, just add the existing URI to the results. */
|
|
||||||
if (existingUris != null) {
|
|
||||||
int index = existingUris.indexOf(mediaUri);
|
|
||||||
if (index != -1) {
|
|
||||||
results.add(mediaUri);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Otherwise, save the media.
|
|
||||||
|
|
||||||
Uri uri = Uri.parse(mediaUri);
|
|
||||||
|
|
||||||
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
|
|
||||||
|
|
||||||
String mimeType = contentResolver.getType(uri);
|
|
||||||
MimeTypeMap map = MimeTypeMap.getSingleton();
|
|
||||||
String fileExtension = map.getExtensionFromMimeType(mimeType);
|
|
||||||
String filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension);
|
|
||||||
File file = new File(directory, filename);
|
|
||||||
filesSoFar.add(file);
|
|
||||||
boolean copied = IOUtils.copyToFile(contentResolver, uri, file);
|
|
||||||
if (!copied) {
|
|
||||||
/* If any media files were created in prior iterations, delete those before
|
|
||||||
* returning. */
|
|
||||||
for (File earlierFile : filesSoFar) {
|
|
||||||
boolean deleted = earlierFile.delete();
|
|
||||||
if (!deleted) {
|
|
||||||
Log.i(TAG, "Could not delete the file " + earlierFile.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
Uri resultUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file);
|
|
||||||
results.add(resultUri.toString());
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void deleteMedia(List<String> mediaUris) {
|
|
||||||
for (String uriString : mediaUris) {
|
|
||||||
Uri uri = Uri.parse(uriString);
|
|
||||||
if (context.getContentResolver().delete(uri, null, null) == 0) {
|
|
||||||
Log.e(TAG, String.format("Did not delete file %s.", uriString));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A∖B={x∈A|x∉B}
|
|
||||||
*
|
|
||||||
* @return all elements of set A that are not in set B.
|
|
||||||
*/
|
|
||||||
private static List<String> setDifference(List<String> a, List<String> b) {
|
|
||||||
List<String> c = new ArrayList<>();
|
|
||||||
for (String s : a) {
|
|
||||||
if (!b.contains(s)) {
|
|
||||||
c.add(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
8
app/src/main/res/drawable/ic_alert_circle.xml
Normal file
8
app/src/main/res/drawable/ic_alert_circle.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:height="24dp"
|
||||||
|
android:width="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#000" android:pathData="M11,15H13V17H11V15M11,7H13V13H11V7M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20Z" />
|
||||||
|
</vector>
|
|
@ -4,5 +4,5 @@
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:viewportHeight="24">
|
android:viewportHeight="24">
|
||||||
<path android:fillColor="#000" android:pathData="M3,7V5H5V4C5,2.89 5.9,2 7,2H13V9L15.5,7.5L18,9V2H19C20.05,2 21,2.95 21,4V20C21,21.05 20.05,22 19,22H7C5.95,22 5,21.05 5,20V19H3V17H5V13H3V11H5V7H3M7,11H5V13H7V11M7,7V5H5V7H7M7,19V17H5V19H7Z" />
|
<path android:fillColor="?attr/colorControlNormal" android:pathData="M3,7V5H5V4C5,2.89 5.9,2 7,2H13V9L15.5,7.5L18,9V2H19C20.05,2 21,2.95 21,4V20C21,21.05 20.05,22 19,22H7C5.95,22 5,21.05 5,20V19H3V17H5V13H3V11H5V7H3M7,11H5V13H7V11M7,7V5H5V7H7M7,19V17H5V19H7Z" />
|
||||||
</vector>
|
</vector>
|
|
@ -1,8 +1,10 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="?attr/windowBackgroundColor">
|
android:background="?attr/windowBackgroundColor"
|
||||||
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/swipeRefreshLayout"
|
android:id="@+id/swipeRefreshLayout"
|
||||||
|
|
|
@ -145,9 +145,12 @@
|
||||||
android:id="@+id/pollPreview"
|
android:id="@+id/pollPreview"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="@dimen/poll_preview_min_width"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|
34
app/src/main/res/layout/activity_drafts.xml
Normal file
34
app/src/main/res/layout/activity_drafts.xml
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".components.drafts.DraftsActivity">
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/includedToolbar"
|
||||||
|
layout="@layout/toolbar_basic" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/draftsRecyclerView"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
|
||||||
|
|
||||||
|
<com.keylesspalace.tusky.view.BackgroundMessageView
|
||||||
|
android:id="@+id/draftsErrorMessageView"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerInParent="true"
|
||||||
|
android:src="@android:color/transparent"
|
||||||
|
android:visibility="gone"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
tools:src="@drawable/elephant_error"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/bottomSheet"
|
||||||
|
layout="@layout/item_status_bottom_sheet" />
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -1,9 +1,11 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/swipeRefreshLayout"
|
android:id="@+id/swipeRefreshLayout"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="top">
|
android:layout_gravity="top"
|
||||||
|
tools:viewBindingIgnore="true">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recyclerView"
|
android:id="@+id/recyclerView"
|
||||||
|
|
95
app/src/main/res/layout/item_draft.xml
Normal file
95
app/src/main/res/layout/item_draft.xml
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:background="?attr/selectableItemBackground"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="4dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/draftSendingInfo"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:drawablePadding="4dp"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:text="@string/drafts_toot_failed_to_send"
|
||||||
|
android:textColor="@color/tusky_red"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:drawableStartCompat="@drawable/ic_alert_circle"
|
||||||
|
app:drawableTint="@color/tusky_red"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/deleteButton"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<androidx.emoji.widget.EmojiTextView
|
||||||
|
android:id="@+id/contentWarning"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:fontFamily="sans-serif-medium"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/deleteButton"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/draftSendingInfo"
|
||||||
|
tools:text="Some content warning" />
|
||||||
|
|
||||||
|
<androidx.emoji.widget.EmojiTextView
|
||||||
|
android:id="@+id/content"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:textSize="?attr/status_text_medium"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/deleteButton"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/contentWarning"
|
||||||
|
tools:text="Some toot content. May be very long." />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/deleteButton"
|
||||||
|
style="@style/TuskyImageButton"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_margin="12dp"
|
||||||
|
android:contentDescription="@string/action_delete"
|
||||||
|
android:padding="4dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_bias="0"
|
||||||
|
app:srcCompat="@drawable/ic_clear_24dp" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/draftMediaPreview"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/content" />
|
||||||
|
|
||||||
|
<com.keylesspalace.tusky.components.compose.view.PollPreviewView
|
||||||
|
android:id="@+id/draftPoll"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
android:minWidth="@dimen/poll_preview_min_width"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/draftMediaPreview"
|
||||||
|
app:layout_goneMarginEnd="8dp" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,19 +1,16 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
<com.google.android.material.appbar.AppBarLayout
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/appbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:elevation="@dimen/actionbar_elevation"
|
||||||
|
app:layout_collapseMode="pin">
|
||||||
|
|
||||||
<com.google.android.material.appbar.AppBarLayout
|
<androidx.appcompat.widget.Toolbar
|
||||||
android:id="@+id/appbar"
|
android:id="@+id/toolbar"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="?attr/actionBarSize" />
|
||||||
android:elevation="@dimen/actionbar_elevation"
|
|
||||||
app:layout_collapseMode="pin">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
</com.google.android.material.appbar.AppBarLayout>
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?attr/actionBarSize" />
|
|
||||||
|
|
||||||
</com.google.android.material.appbar.AppBarLayout>
|
|
||||||
|
|
||||||
</merge>
|
|
||||||
|
|
10
app/src/main/res/menu/drafts.xml
Normal file
10
app/src/main/res/menu/drafts.xml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_old_drafts"
|
||||||
|
android:icon="@drawable/ic_notebook"
|
||||||
|
android:title="@string/old_drafts"
|
||||||
|
android:visible="false"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
</menu>
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">الحسابات المحظورة</string>
|
<string name="title_blocks">الحسابات المحظورة</string>
|
||||||
<string name="title_follow_requests">طلبات المتابعة</string>
|
<string name="title_follow_requests">طلبات المتابعة</string>
|
||||||
<string name="title_edit_profile">عدل ملفك التعريفي</string>
|
<string name="title_edit_profile">عدل ملفك التعريفي</string>
|
||||||
<string name="title_saved_toot">المسودات</string>
|
<string name="title_drafts">المسودات</string>
|
||||||
<string name="title_licenses">الرّخص</string>
|
<string name="title_licenses">الرّخص</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">شارَكَه %s</string>
|
<string name="status_boosted_format">شارَكَه %s</string>
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
<string name="action_search">ⵏⴰⴸⵉ</string>
|
<string name="action_search">ⵏⴰⴸⵉ</string>
|
||||||
<string name="action_edit_profile">ⵣⵔⴻⴳ ⴰⵎⴰⵖⵏⵓ</string>
|
<string name="action_edit_profile">ⵣⵔⴻⴳ ⴰⵎⴰⵖⵏⵓ</string>
|
||||||
<string name="action_logout">ⴼⴼⴻⵖ</string>
|
<string name="action_logout">ⴼⴼⴻⵖ</string>
|
||||||
<string name="title_saved_toot">ⵉⵔⴻⵡⵡⴰⵢⴻⵏ</string>
|
<string name="title_drafts">ⵉⵔⴻⵡⵡⴰⵢⴻⵏ</string>
|
||||||
<string name="title_favourites">ⵉⵙⵎⴻⵏⵢⵉⴼⴻⵏ</string>
|
<string name="title_favourites">ⵉⵙⵎⴻⵏⵢⵉⴼⴻⵏ</string>
|
||||||
<string name="button_back">ⵓⵖⴰⵍ</string>
|
<string name="button_back">ⵓⵖⴰⵍ</string>
|
||||||
<string name="button_continue">ⴽⴻⵎⵎⴻⵍ</string>
|
<string name="button_continue">ⴽⴻⵎⵎⴻⵍ</string>
|
||||||
|
|
|
@ -292,7 +292,7 @@
|
||||||
<string name="status_media_hidden_title">মিডিয়া লুকানো</string>
|
<string name="status_media_hidden_title">মিডিয়া লুকানো</string>
|
||||||
<string name="status_sensitive_media_title">সংবেদনশীল কন্টেন্ট</string>
|
<string name="status_sensitive_media_title">সংবেদনশীল কন্টেন্ট</string>
|
||||||
<string name="title_licenses">লাইসেন্সগুলি</string>
|
<string name="title_licenses">লাইসেন্সগুলি</string>
|
||||||
<string name="title_saved_toot">খসড়াগুলো</string>
|
<string name="title_drafts">খসড়াগুলো</string>
|
||||||
<string name="title_edit_profile">আপনার প্রোফাইল সম্পাদনা করুন</string>
|
<string name="title_edit_profile">আপনার প্রোফাইল সম্পাদনা করুন</string>
|
||||||
<string name="title_follow_requests">অনুরোধ অনুসরণ করুন</string>
|
<string name="title_follow_requests">অনুরোধ অনুসরণ করুন</string>
|
||||||
<string name="title_blocks">অবরুদ্ধ ব্যবহারকারী</string>
|
<string name="title_blocks">অবরুদ্ধ ব্যবহারকারী</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">অবরুদ্ধ ব্যবহারকারী</string>
|
<string name="title_blocks">অবরুদ্ধ ব্যবহারকারী</string>
|
||||||
<string name="title_follow_requests">অনুরোধ অনুসরণ করুন</string>
|
<string name="title_follow_requests">অনুরোধ অনুসরণ করুন</string>
|
||||||
<string name="title_edit_profile">আপনার প্রোফাইল সম্পাদনা করুন</string>
|
<string name="title_edit_profile">আপনার প্রোফাইল সম্পাদনা করুন</string>
|
||||||
<string name="title_saved_toot">খসড়াগুলো</string>
|
<string name="title_drafts">খসড়াগুলো</string>
|
||||||
<string name="title_licenses">লাইসেন্সগুলি</string>
|
<string name="title_licenses">লাইসেন্সগুলি</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s সমর্থন দিয়েছে</string>
|
<string name="status_boosted_format">%s সমর্থন দিয়েছে</string>
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
<string name="title_blocks">Usuaris blocats</string>
|
<string name="title_blocks">Usuaris blocats</string>
|
||||||
<string name="title_follow_requests">Peticions de seguiment</string>
|
<string name="title_follow_requests">Peticions de seguiment</string>
|
||||||
<string name="title_edit_profile">Edita el perfil</string>
|
<string name="title_edit_profile">Edita el perfil</string>
|
||||||
<string name="title_saved_toot">Esborranys</string>
|
<string name="title_drafts">Esborranys</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s tootejat</string>
|
<string name="status_boosted_format">%s tootejat</string>
|
||||||
<string name="status_sensitive_media_title">Contingut sensible</string>
|
<string name="status_sensitive_media_title">Contingut sensible</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">Blokovaní uživatelé</string>
|
<string name="title_blocks">Blokovaní uživatelé</string>
|
||||||
<string name="title_follow_requests">Žádosti o sledování</string>
|
<string name="title_follow_requests">Žádosti o sledování</string>
|
||||||
<string name="title_edit_profile">Upravit váš profil</string>
|
<string name="title_edit_profile">Upravit váš profil</string>
|
||||||
<string name="title_saved_toot">Koncepty</string>
|
<string name="title_drafts">Koncepty</string>
|
||||||
<string name="title_licenses">Licence</string>
|
<string name="title_licenses">Licence</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s boostnul/a</string>
|
<string name="status_boosted_format">%s boostnul/a</string>
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<string name="title_blocks">Defnyddwyr wedi\'u blocio</string>
|
<string name="title_blocks">Defnyddwyr wedi\'u blocio</string>
|
||||||
<string name="title_follow_requests">Dilyn ceisiadau</string>
|
<string name="title_follow_requests">Dilyn ceisiadau</string>
|
||||||
<string name="title_edit_profile">Golygu\'ch Proffil</string>
|
<string name="title_edit_profile">Golygu\'ch Proffil</string>
|
||||||
<string name="title_saved_toot">Drafftiau</string>
|
<string name="title_drafts">Drafftiau</string>
|
||||||
<string name="title_licenses">Trwyddedau</string>
|
<string name="title_licenses">Trwyddedau</string>
|
||||||
<string name="status_boosted_format">%s wedi\'u hybu</string>
|
<string name="status_boosted_format">%s wedi\'u hybu</string>
|
||||||
<string name="status_sensitive_media_title">Cynnwys sensitif</string>
|
<string name="status_sensitive_media_title">Cynnwys sensitif</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">Blockierte Profile</string>
|
<string name="title_blocks">Blockierte Profile</string>
|
||||||
<string name="title_follow_requests">Folgeanfragen</string>
|
<string name="title_follow_requests">Folgeanfragen</string>
|
||||||
<string name="title_edit_profile">Dein Profil bearbeiten</string>
|
<string name="title_edit_profile">Dein Profil bearbeiten</string>
|
||||||
<string name="title_saved_toot">Entwürfe</string>
|
<string name="title_drafts">Entwürfe</string>
|
||||||
<string name="title_licenses">Lizenzen</string>
|
<string name="title_licenses">Lizenzen</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s teilte</string>
|
<string name="status_boosted_format">%s teilte</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">Blokitaj uzantoj</string>
|
<string name="title_blocks">Blokitaj uzantoj</string>
|
||||||
<string name="title_follow_requests">Petoj de sekvado</string>
|
<string name="title_follow_requests">Petoj de sekvado</string>
|
||||||
<string name="title_edit_profile">Redakti vian profilon</string>
|
<string name="title_edit_profile">Redakti vian profilon</string>
|
||||||
<string name="title_saved_toot">Malnetoj</string>
|
<string name="title_drafts">Malnetoj</string>
|
||||||
<string name="title_licenses">Permesiloj</string>
|
<string name="title_licenses">Permesiloj</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s diskonigis</string>
|
<string name="status_boosted_format">%s diskonigis</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">Bloqueados</string>
|
<string name="title_blocks">Bloqueados</string>
|
||||||
<string name="title_follow_requests">Solicitudes</string>
|
<string name="title_follow_requests">Solicitudes</string>
|
||||||
<string name="title_edit_profile">Editar tu perfil</string>
|
<string name="title_edit_profile">Editar tu perfil</string>
|
||||||
<string name="title_saved_toot">Borradores</string>
|
<string name="title_drafts">Borradores</string>
|
||||||
<string name="title_licenses">Licencias</string>
|
<string name="title_licenses">Licencias</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s compartió</string>
|
<string name="status_boosted_format">%s compartió</string>
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<string name="title_blocks">Blokeatuak</string>
|
<string name="title_blocks">Blokeatuak</string>
|
||||||
<string name="title_follow_requests">Eskakizunak</string>
|
<string name="title_follow_requests">Eskakizunak</string>
|
||||||
<string name="title_edit_profile">Profila editatu</string>
|
<string name="title_edit_profile">Profila editatu</string>
|
||||||
<string name="title_saved_toot">Zirriborroak</string>
|
<string name="title_drafts">Zirriborroak</string>
|
||||||
<string name="title_licenses">Lizentziak</string>
|
<string name="title_licenses">Lizentziak</string>
|
||||||
<string name="status_boosted_format">%s-(e)k bultzatu du</string>
|
<string name="status_boosted_format">%s-(e)k bultzatu du</string>
|
||||||
<string name="status_sensitive_media_title">Kontuz edukiarekin</string>
|
<string name="status_sensitive_media_title">Kontuz edukiarekin</string>
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<string name="title_blocks">کاربران مسدود</string>
|
<string name="title_blocks">کاربران مسدود</string>
|
||||||
<string name="title_follow_requests">درخواستهای پیگیری</string>
|
<string name="title_follow_requests">درخواستهای پیگیری</string>
|
||||||
<string name="title_edit_profile">ویرایش نمایهتان</string>
|
<string name="title_edit_profile">ویرایش نمایهتان</string>
|
||||||
<string name="title_saved_toot">پیشنویسها</string>
|
<string name="title_drafts">پیشنویسها</string>
|
||||||
<string name="title_licenses">پروانهها</string>
|
<string name="title_licenses">پروانهها</string>
|
||||||
<string name="status_boosted_format">%s تقویت کرد</string>
|
<string name="status_boosted_format">%s تقویت کرد</string>
|
||||||
<string name="status_sensitive_media_title">محتوای حسّاس</string>
|
<string name="status_sensitive_media_title">محتوای حسّاس</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">Comptes bloqués</string>
|
<string name="title_blocks">Comptes bloqués</string>
|
||||||
<string name="title_follow_requests">Demandes d’abonnement</string>
|
<string name="title_follow_requests">Demandes d’abonnement</string>
|
||||||
<string name="title_edit_profile">Modifier votre profil</string>
|
<string name="title_edit_profile">Modifier votre profil</string>
|
||||||
<string name="title_saved_toot">Brouillons</string>
|
<string name="title_drafts">Brouillons</string>
|
||||||
<string name="title_licenses">Licences</string>
|
<string name="title_licenses">Licences</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s a partagé</string>
|
<string name="status_boosted_format">%s a partagé</string>
|
||||||
|
|
|
@ -177,7 +177,7 @@
|
||||||
<string name="action_view_account_preferences">Roghanna Cuntais</string>
|
<string name="action_view_account_preferences">Roghanna Cuntais</string>
|
||||||
<string name="action_view_preferences">Sainroghanna</string>
|
<string name="action_view_preferences">Sainroghanna</string>
|
||||||
<string name="action_logout">Logáil Amach</string>
|
<string name="action_logout">Logáil Amach</string>
|
||||||
<string name="title_saved_toot">Dréachtaí</string>
|
<string name="title_drafts">Dréachtaí</string>
|
||||||
<string name="title_favourites">Roghaí</string>
|
<string name="title_favourites">Roghaí</string>
|
||||||
<string name="error_failed_app_registration">Theip ar fhíordheimhniú leis an gcás sin.</string>
|
<string name="error_failed_app_registration">Theip ar fhíordheimhniú leis an gcás sin.</string>
|
||||||
<string name="link_whats_an_instance">Cad is sampla ann\?</string>
|
<string name="link_whats_an_instance">Cad is sampla ann\?</string>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<string name="action_view_account_preferences">Roighainnean cunntais</string>
|
<string name="action_view_account_preferences">Roighainnean cunntais</string>
|
||||||
<string name="action_view_preferences">Roighainnean</string>
|
<string name="action_view_preferences">Roighainnean</string>
|
||||||
<string name="action_logout">Clàraich a-mach</string>
|
<string name="action_logout">Clàraich a-mach</string>
|
||||||
<string name="title_saved_toot">Dreachd</string>
|
<string name="title_drafts">Dreachd</string>
|
||||||
<string name="title_favourites">Prìomhaich</string>
|
<string name="title_favourites">Prìomhaich</string>
|
||||||
<string name="link_whats_an_instance">Dè a th ’ann an àite\?</string>
|
<string name="link_whats_an_instance">Dè a th ’ann an àite\?</string>
|
||||||
<string name="edit_poll">Deasaich</string>
|
<string name="edit_poll">Deasaich</string>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="action_login">हिंदी</string>
|
<string name="action_login">हिंदी</string>
|
||||||
<string name="title_favourites">पसंदीदा</string>
|
<string name="title_favourites">पसंदीदा</string>
|
||||||
<string name="title_saved_toot">प्रारूप</string>
|
<string name="title_drafts">प्रारूप</string>
|
||||||
<string name="action_logout">लॉग आउट</string>
|
<string name="action_logout">लॉग आउट</string>
|
||||||
<string name="action_view_preferences">पसंद</string>
|
<string name="action_view_preferences">पसंद</string>
|
||||||
<string name="action_view_account_preferences">खाता प्राथमिकताएं</string>
|
<string name="action_view_account_preferences">खाता प्राथमिकताएं</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">Letiltott felhasználók</string>
|
<string name="title_blocks">Letiltott felhasználók</string>
|
||||||
<string name="title_follow_requests">Követési kérelmek</string>
|
<string name="title_follow_requests">Követési kérelmek</string>
|
||||||
<string name="title_edit_profile">Profilod szerkesztése</string>
|
<string name="title_edit_profile">Profilod szerkesztése</string>
|
||||||
<string name="title_saved_toot">Piszkozatok</string>
|
<string name="title_drafts">Piszkozatok</string>
|
||||||
<string name="title_licenses">Licenszek</string>
|
<string name="title_licenses">Licenszek</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s megtolta</string>
|
<string name="status_boosted_format">%s megtolta</string>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<string name="action_login">Skrá inn með Mastodon</string>
|
<string name="action_login">Skrá inn með Mastodon</string>
|
||||||
<string name="link_whats_an_instance">Hvað er tilvik\?</string>
|
<string name="link_whats_an_instance">Hvað er tilvik\?</string>
|
||||||
<string name="title_favourites">Eftirlæti</string>
|
<string name="title_favourites">Eftirlæti</string>
|
||||||
<string name="title_saved_toot">Drög</string>
|
<string name="title_drafts">Drög</string>
|
||||||
<string name="action_logout">Skrá út</string>
|
<string name="action_logout">Skrá út</string>
|
||||||
<string name="action_view_preferences">Kjörstillingar</string>
|
<string name="action_view_preferences">Kjörstillingar</string>
|
||||||
<string name="action_view_account_preferences">Eiginleikar tengingar</string>
|
<string name="action_view_account_preferences">Eiginleikar tengingar</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">Utenti bloccati</string>
|
<string name="title_blocks">Utenti bloccati</string>
|
||||||
<string name="title_follow_requests">Richieste di seguirti</string>
|
<string name="title_follow_requests">Richieste di seguirti</string>
|
||||||
<string name="title_edit_profile">Modifica il tuo profilo</string>
|
<string name="title_edit_profile">Modifica il tuo profilo</string>
|
||||||
<string name="title_saved_toot">Bozze</string>
|
<string name="title_drafts">Bozze</string>
|
||||||
<string name="title_licenses">Licenze</string>
|
<string name="title_licenses">Licenze</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s ha boostato</string>
|
<string name="status_boosted_format">%s ha boostato</string>
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
<string name="title_blocks">ブロックしたユーザー</string>
|
<string name="title_blocks">ブロックしたユーザー</string>
|
||||||
<string name="title_follow_requests">フォローリクエスト</string>
|
<string name="title_follow_requests">フォローリクエスト</string>
|
||||||
<string name="title_edit_profile">プロフィールを編集</string>
|
<string name="title_edit_profile">プロフィールを編集</string>
|
||||||
<string name="title_saved_toot">下書き</string>
|
<string name="title_drafts">下書き</string>
|
||||||
<string name="title_licenses">ライセンス</string>
|
<string name="title_licenses">ライセンス</string>
|
||||||
<string name="status_boosted_format">%sさんがブーストしました</string>
|
<string name="status_boosted_format">%sさんがブーストしました</string>
|
||||||
<string name="status_sensitive_media_title">閲覧注意</string>
|
<string name="status_sensitive_media_title">閲覧注意</string>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<resources>
|
<resources>
|
||||||
<string name="action_login">Qqen ɣer Maṣṭudun</string>
|
<string name="action_login">Qqen ɣer Maṣṭudun</string>
|
||||||
<string name="title_favourites">Ismenyifen</string>
|
<string name="title_favourites">Ismenyifen</string>
|
||||||
<string name="title_saved_toot">Irewwayen</string>
|
<string name="title_drafts">Irewwayen</string>
|
||||||
<string name="action_logout">Ffeɣ</string>
|
<string name="action_logout">Ffeɣ</string>
|
||||||
<string name="action_view_preferences">Iɣewwaṛen</string>
|
<string name="action_view_preferences">Iɣewwaṛen</string>
|
||||||
<string name="action_view_account_preferences">Iɣewwaṛen n umiḍan</string>
|
<string name="action_view_account_preferences">Iɣewwaṛen n umiḍan</string>
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
<string name="title_domain_mutes">숨긴 도메인</string>
|
<string name="title_domain_mutes">숨긴 도메인</string>
|
||||||
<string name="title_follow_requests">팔로우 요청</string>
|
<string name="title_follow_requests">팔로우 요청</string>
|
||||||
<string name="title_edit_profile">프로필 편집</string>
|
<string name="title_edit_profile">프로필 편집</string>
|
||||||
<string name="title_saved_toot">임시 저장</string>
|
<string name="title_drafts">임시 저장</string>
|
||||||
<string name="title_licenses">라이선스</string>
|
<string name="title_licenses">라이선스</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s님이 부스트 했습니다</string>
|
<string name="status_boosted_format">%s님이 부스트 했습니다</string>
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<string name="action_login">മസ്റ്റഡോൺ വഴി പ്രവേശിക്കുക</string>
|
<string name="action_login">മസ്റ്റഡോൺ വഴി പ്രവേശിക്കുക</string>
|
||||||
<string name="link_whats_an_instance">എന്താണ് ഒരു ഇൻസ്റ്റൻസ്\?</string>
|
<string name="link_whats_an_instance">എന്താണ് ഒരു ഇൻസ്റ്റൻസ്\?</string>
|
||||||
<string name="title_favourites">പ്രിയപ്പെട്ടവ</string>
|
<string name="title_favourites">പ്രിയപ്പെട്ടവ</string>
|
||||||
<string name="title_saved_toot">കരടുകൾ</string>
|
<string name="title_drafts">കരടുകൾ</string>
|
||||||
<string name="action_logout">പുറത്തിറങ്ങുക</string>
|
<string name="action_logout">പുറത്തിറങ്ങുക</string>
|
||||||
<string name="action_view_preferences">മുൻഗണനകൾ</string>
|
<string name="action_view_preferences">മുൻഗണനകൾ</string>
|
||||||
<string name="action_view_account_preferences">അക്കൗണ്ട് മുൻഗണനകൾ</string>
|
<string name="action_view_account_preferences">അക്കൗണ്ട് മുൻഗണനകൾ</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">Geblokkeerde gebruikers</string>
|
<string name="title_blocks">Geblokkeerde gebruikers</string>
|
||||||
<string name="title_follow_requests">Volgverzoeken</string>
|
<string name="title_follow_requests">Volgverzoeken</string>
|
||||||
<string name="title_edit_profile">Profiel bewerken</string>
|
<string name="title_edit_profile">Profiel bewerken</string>
|
||||||
<string name="title_saved_toot">Concepten</string>
|
<string name="title_drafts">Concepten</string>
|
||||||
<string name="title_licenses">Licenties</string>
|
<string name="title_licenses">Licenties</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s boostte</string>
|
<string name="status_boosted_format">%s boostte</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">Blokkerte brukere</string>
|
<string name="title_blocks">Blokkerte brukere</string>
|
||||||
<string name="title_follow_requests">Forespørsler om følgen</string>
|
<string name="title_follow_requests">Forespørsler om følgen</string>
|
||||||
<string name="title_edit_profile">Endre profilen din</string>
|
<string name="title_edit_profile">Endre profilen din</string>
|
||||||
<string name="title_saved_toot">Kladder</string>
|
<string name="title_drafts">Kladder</string>
|
||||||
<string name="title_licenses">Lisenser</string>
|
<string name="title_licenses">Lisenser</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s boosted</string>
|
<string name="status_boosted_format">%s boosted</string>
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<string name="title_blocks">Utilizaires blocats</string>
|
<string name="title_blocks">Utilizaires blocats</string>
|
||||||
<string name="title_follow_requests">Demandas d’abonament</string>
|
<string name="title_follow_requests">Demandas d’abonament</string>
|
||||||
<string name="title_edit_profile">Modificar lo perfil</string>
|
<string name="title_edit_profile">Modificar lo perfil</string>
|
||||||
<string name="title_saved_toot">Borrolhons</string>
|
<string name="title_drafts">Borrolhons</string>
|
||||||
<string name="title_licenses">Licéncias</string>
|
<string name="title_licenses">Licéncias</string>
|
||||||
<string name="status_boosted_format">%s partejat</string>
|
<string name="status_boosted_format">%s partejat</string>
|
||||||
<string name="status_sensitive_media_title">Contengut sensible</string>
|
<string name="status_sensitive_media_title">Contengut sensible</string>
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<string name="title_blocks">Zablokowani użytkownicy</string>
|
<string name="title_blocks">Zablokowani użytkownicy</string>
|
||||||
<string name="title_follow_requests">Prośby o możliwość śledzenia</string>
|
<string name="title_follow_requests">Prośby o możliwość śledzenia</string>
|
||||||
<string name="title_edit_profile">Edytuj profil</string>
|
<string name="title_edit_profile">Edytuj profil</string>
|
||||||
<string name="title_saved_toot">Szkice</string>
|
<string name="title_drafts">Szkice</string>
|
||||||
<string name="title_licenses">Licencje</string>
|
<string name="title_licenses">Licencje</string>
|
||||||
<string name="status_boosted_format">%s podbił</string>
|
<string name="status_boosted_format">%s podbił</string>
|
||||||
<string name="status_sensitive_media_title">Wrażliwe treści</string>
|
<string name="status_sensitive_media_title">Wrażliwe treści</string>
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
<string name="title_blocks">Usuários bloqueados</string>
|
<string name="title_blocks">Usuários bloqueados</string>
|
||||||
<string name="title_follow_requests">Seguidores pendentes</string>
|
<string name="title_follow_requests">Seguidores pendentes</string>
|
||||||
<string name="title_edit_profile">Editar perfil</string>
|
<string name="title_edit_profile">Editar perfil</string>
|
||||||
<string name="title_saved_toot">Rascunhos</string>
|
<string name="title_drafts">Rascunhos</string>
|
||||||
<string name="title_licenses">Licenças</string>
|
<string name="title_licenses">Licenças</string>
|
||||||
<string name="status_boosted_format">%s deu boost</string>
|
<string name="status_boosted_format">%s deu boost</string>
|
||||||
<string name="status_sensitive_media_title">Mídia sensível</string>
|
<string name="status_sensitive_media_title">Mídia sensível</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">Список блокировки</string>
|
<string name="title_blocks">Список блокировки</string>
|
||||||
<string name="title_follow_requests">Запросы на подписку</string>
|
<string name="title_follow_requests">Запросы на подписку</string>
|
||||||
<string name="title_edit_profile">Редактировать профиль</string>
|
<string name="title_edit_profile">Редактировать профиль</string>
|
||||||
<string name="title_saved_toot">Черновики</string>
|
<string name="title_drafts">Черновики</string>
|
||||||
<string name="title_licenses">Лицензии</string>
|
<string name="title_licenses">Лицензии</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s продвинул(а)</string>
|
<string name="status_boosted_format">%s продвинул(а)</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="title_licenses">अनुज्ञापत्राणि</string>
|
<string name="title_licenses">अनुज्ञापत्राणि</string>
|
||||||
<string name="title_scheduled_toot">कालबद्धदौत्यानि</string>
|
<string name="title_scheduled_toot">कालबद्धदौत्यानि</string>
|
||||||
<string name="title_saved_toot">लेखविकर्षाः</string>
|
<string name="title_drafts">लेखविकर्षाः</string>
|
||||||
<string name="title_edit_profile">स्वीयव्यक्तिविवरणं सम्पाद्यताम्</string>
|
<string name="title_edit_profile">स्वीयव्यक्तिविवरणं सम्पाद्यताम्</string>
|
||||||
<string name="title_follow_requests">अनुसरणार्थमनुरोधाः</string>
|
<string name="title_follow_requests">अनुसरणार्थमनुरोधाः</string>
|
||||||
<string name="title_domain_mutes">प्रच्छन्नप्रदेशाः</string>
|
<string name="title_domain_mutes">प्रच्छन्नप्रदेशाः</string>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="action_login">Prihlásiť sa účtom Mastodon</string>
|
<string name="action_login">Prihlásiť sa účtom Mastodon</string>
|
||||||
<string name="title_saved_toot">Koncepty</string>
|
<string name="title_drafts">Koncepty</string>
|
||||||
<string name="action_logout">Odhlásiť sa</string>
|
<string name="action_logout">Odhlásiť sa</string>
|
||||||
<string name="action_view_preferences">Nastavenia</string>
|
<string name="action_view_preferences">Nastavenia</string>
|
||||||
<string name="action_view_account_preferences">Nastavenia účtu</string>
|
<string name="action_view_account_preferences">Nastavenia účtu</string>
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
<string name="title_blocks">Blokirani uporabniki</string>
|
<string name="title_blocks">Blokirani uporabniki</string>
|
||||||
<string name="title_follow_requests">Zahteve za Sledenje</string>
|
<string name="title_follow_requests">Zahteve za Sledenje</string>
|
||||||
<string name="title_edit_profile">Uredi svoj profil</string>
|
<string name="title_edit_profile">Uredi svoj profil</string>
|
||||||
<string name="title_saved_toot">Osnutki</string>
|
<string name="title_drafts">Osnutki</string>
|
||||||
<string name="title_licenses">Licence</string>
|
<string name="title_licenses">Licence</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_sensitive_media_title">Občutljiva vsebina</string>
|
<string name="status_sensitive_media_title">Občutljiva vsebina</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">Blockerade användare</string>
|
<string name="title_blocks">Blockerade användare</string>
|
||||||
<string name="title_follow_requests">Följarförfrågningar</string>
|
<string name="title_follow_requests">Följarförfrågningar</string>
|
||||||
<string name="title_edit_profile">Ändra din profil</string>
|
<string name="title_edit_profile">Ändra din profil</string>
|
||||||
<string name="title_saved_toot">Utkast</string>
|
<string name="title_drafts">Utkast</string>
|
||||||
<string name="title_licenses">Licenser</string>
|
<string name="title_licenses">Licenser</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s knuffade</string>
|
<string name="status_boosted_format">%s knuffade</string>
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
<string name="title_blocks">தடைசெய்யபட்ட பயனர்கள்</string>
|
<string name="title_blocks">தடைசெய்யபட்ட பயனர்கள்</string>
|
||||||
<string name="title_follow_requests">பின்பற்ற கோரிக்கை</string>
|
<string name="title_follow_requests">பின்பற்ற கோரிக்கை</string>
|
||||||
<string name="title_edit_profile">சுயவிவரத்தை திருத்த</string>
|
<string name="title_edit_profile">சுயவிவரத்தை திருத்த</string>
|
||||||
<string name="title_saved_toot">வரைவுகள்</string>
|
<string name="title_drafts">வரைவுகள்</string>
|
||||||
<string name="status_boosted_format">%s மேலேற்றப்பட்டது</string>
|
<string name="status_boosted_format">%s மேலேற்றப்பட்டது</string>
|
||||||
<string name="status_sensitive_media_title">உணர்ச்சிகரமான உள்ளடக்கம்</string>
|
<string name="status_sensitive_media_title">உணர்ச்சிகரமான உள்ளடக்கம்</string>
|
||||||
<string name="status_media_hidden_title">ஊடகம் மறைக்கப்பட்டது</string>
|
<string name="status_media_hidden_title">ஊடகம் மறைக்கப்பட்டது</string>
|
||||||
|
|
|
@ -431,7 +431,7 @@
|
||||||
<string name="action_view_account_preferences">ตั้งค่าบัญชี</string>
|
<string name="action_view_account_preferences">ตั้งค่าบัญชี</string>
|
||||||
<string name="action_view_preferences">ตั้งค่า</string>
|
<string name="action_view_preferences">ตั้งค่า</string>
|
||||||
<string name="action_logout">ออกจากระบบ</string>
|
<string name="action_logout">ออกจากระบบ</string>
|
||||||
<string name="title_saved_toot">ฉบับร่าง</string>
|
<string name="title_drafts">ฉบับร่าง</string>
|
||||||
<string name="title_favourites">ชื่นชอบ</string>
|
<string name="title_favourites">ชื่นชอบ</string>
|
||||||
<string name="error_failed_app_registration">การยืนยันตัวตนกับเซิร์ฟเวอร์นั้นล้มเหลว</string>
|
<string name="error_failed_app_registration">การยืนยันตัวตนกับเซิร์ฟเวอร์นั้นล้มเหลว</string>
|
||||||
<string name="link_whats_an_instance">Instance คือ\?</string>
|
<string name="link_whats_an_instance">Instance คือ\?</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">Engellenmiş kullanıcılar</string>
|
<string name="title_blocks">Engellenmiş kullanıcılar</string>
|
||||||
<string name="title_follow_requests">Takip Etme İstekleri</string>
|
<string name="title_follow_requests">Takip Etme İstekleri</string>
|
||||||
<string name="title_edit_profile">Profili düzeltme</string>
|
<string name="title_edit_profile">Profili düzeltme</string>
|
||||||
<string name="title_saved_toot">Taslaklar</string>
|
<string name="title_drafts">Taslaklar</string>
|
||||||
<string name="title_licenses">Lisanslar</string>
|
<string name="title_licenses">Lisanslar</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s yineledi</string>
|
<string name="status_boosted_format">%s yineledi</string>
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
<string name="action_view_account_preferences">Налаштування акаунта</string>
|
<string name="action_view_account_preferences">Налаштування акаунта</string>
|
||||||
<string name="action_view_preferences">Налаштування</string>
|
<string name="action_view_preferences">Налаштування</string>
|
||||||
<string name="action_logout">Вийти</string>
|
<string name="action_logout">Вийти</string>
|
||||||
<string name="title_saved_toot">Чернетки</string>
|
<string name="title_drafts">Чернетки</string>
|
||||||
<string name="title_favourites">Вподобане</string>
|
<string name="title_favourites">Вподобане</string>
|
||||||
<string name="action_login">Увійти</string>
|
<string name="action_login">Увійти</string>
|
||||||
<string name="login_connection">Зʼєднання…</string>
|
<string name="login_connection">Зʼєднання…</string>
|
||||||
|
|
|
@ -197,7 +197,7 @@
|
||||||
<string name="title_public_local">Cộng đồng</string>
|
<string name="title_public_local">Cộng đồng</string>
|
||||||
<string name="title_notifications">Thông báo</string>
|
<string name="title_notifications">Thông báo</string>
|
||||||
<string name="title_home">Bảng tin</string>
|
<string name="title_home">Bảng tin</string>
|
||||||
<string name="title_saved_toot">Nháp</string>
|
<string name="title_drafts">Nháp</string>
|
||||||
<string name="title_favourites">Lượt thích</string>
|
<string name="title_favourites">Lượt thích</string>
|
||||||
<string name="link_whats_an_instance">Máy chủ là gì\?</string>
|
<string name="link_whats_an_instance">Máy chủ là gì\?</string>
|
||||||
<string name="pref_title_show_media_preview">Tải xem trước hình ảnh</string>
|
<string name="pref_title_show_media_preview">Tải xem trước hình ảnh</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">被屏蔽的用户</string>
|
<string name="title_blocks">被屏蔽的用户</string>
|
||||||
<string name="title_follow_requests">关注请求</string>
|
<string name="title_follow_requests">关注请求</string>
|
||||||
<string name="title_edit_profile">编辑个人资料</string>
|
<string name="title_edit_profile">编辑个人资料</string>
|
||||||
<string name="title_saved_toot">草稿</string>
|
<string name="title_drafts">草稿</string>
|
||||||
<string name="title_licenses">开源协议</string>
|
<string name="title_licenses">开源协议</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s 转嘟了</string>
|
<string name="status_boosted_format">%s 转嘟了</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">被封鎖的使用者</string>
|
<string name="title_blocks">被封鎖的使用者</string>
|
||||||
<string name="title_follow_requests">關注請求</string>
|
<string name="title_follow_requests">關注請求</string>
|
||||||
<string name="title_edit_profile">編輯個人資料</string>
|
<string name="title_edit_profile">編輯個人資料</string>
|
||||||
<string name="title_saved_toot">草稿</string>
|
<string name="title_drafts">草稿</string>
|
||||||
<string name="title_licenses">開源授權</string>
|
<string name="title_licenses">開源授權</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s 轉嘟了</string>
|
<string name="status_boosted_format">%s 轉嘟了</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">被封鎖的使用者</string>
|
<string name="title_blocks">被封鎖的使用者</string>
|
||||||
<string name="title_follow_requests">關注請求</string>
|
<string name="title_follow_requests">關注請求</string>
|
||||||
<string name="title_edit_profile">編輯個人資料</string>
|
<string name="title_edit_profile">編輯個人資料</string>
|
||||||
<string name="title_saved_toot">草稿</string>
|
<string name="title_drafts">草稿</string>
|
||||||
<string name="title_licenses">開源授權</string>
|
<string name="title_licenses">開源授權</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s 轉嘟了</string>
|
<string name="status_boosted_format">%s 轉嘟了</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">被屏蔽的用户</string>
|
<string name="title_blocks">被屏蔽的用户</string>
|
||||||
<string name="title_follow_requests">关注请求</string>
|
<string name="title_follow_requests">关注请求</string>
|
||||||
<string name="title_edit_profile">编辑个人资料</string>
|
<string name="title_edit_profile">编辑个人资料</string>
|
||||||
<string name="title_saved_toot">草稿</string>
|
<string name="title_drafts">草稿</string>
|
||||||
<string name="title_licenses">开源协议</string>
|
<string name="title_licenses">开源协议</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s 转嘟了</string>
|
<string name="status_boosted_format">%s 转嘟了</string>
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
<string name="title_blocks">被封鎖的使用者</string>
|
<string name="title_blocks">被封鎖的使用者</string>
|
||||||
<string name="title_follow_requests">關注請求</string>
|
<string name="title_follow_requests">關注請求</string>
|
||||||
<string name="title_edit_profile">編輯個人資料</string>
|
<string name="title_edit_profile">編輯個人資料</string>
|
||||||
<string name="title_saved_toot">草稿</string>
|
<string name="title_drafts">草稿</string>
|
||||||
<string name="title_licenses">開源授權</string>
|
<string name="title_licenses">開源授權</string>
|
||||||
<string name="status_username_format">\@%s</string>
|
<string name="status_username_format">\@%s</string>
|
||||||
<string name="status_boosted_format">%s 轉嘟了</string>
|
<string name="status_boosted_format">%s 轉嘟了</string>
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
<dimen name="card_radius">5dp</dimen>
|
<dimen name="card_radius">5dp</dimen>
|
||||||
|
|
||||||
<dimen name="poll_preview_padding">12dp</dimen>
|
<dimen name="poll_preview_padding">12dp</dimen>
|
||||||
|
<dimen name="poll_preview_min_width">120dp</dimen>
|
||||||
|
|
||||||
<dimen name="adaptive_bitmap_inner_size">72dp</dimen>
|
<dimen name="adaptive_bitmap_inner_size">72dp</dimen>
|
||||||
<dimen name="adaptive_bitmap_outer_size">108dp</dimen>
|
<dimen name="adaptive_bitmap_outer_size">108dp</dimen>
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
<string name="title_domain_mutes">Hidden domains</string>
|
<string name="title_domain_mutes">Hidden domains</string>
|
||||||
<string name="title_follow_requests">Follow Requests</string>
|
<string name="title_follow_requests">Follow Requests</string>
|
||||||
<string name="title_edit_profile">Edit your profile</string>
|
<string name="title_edit_profile">Edit your profile</string>
|
||||||
<string name="title_saved_toot">Drafts</string>
|
<string name="title_drafts">Drafts</string>
|
||||||
<string name="title_scheduled_toot">Scheduled toots</string>
|
<string name="title_scheduled_toot">Scheduled toots</string>
|
||||||
<string name="title_announcements">Announcements</string>
|
<string name="title_announcements">Announcements</string>
|
||||||
<string name="title_licenses">Licenses</string>
|
<string name="title_licenses">Licenses</string>
|
||||||
|
@ -585,6 +585,7 @@
|
||||||
<string name="pref_title_wellbeing_mode">Wellbeing</string>
|
<string name="pref_title_wellbeing_mode">Wellbeing</string>
|
||||||
<string name="account_note_hint">Your private note about this account</string>
|
<string name="account_note_hint">Your private note about this account</string>
|
||||||
<string name="account_note_saved">Saved!</string>
|
<string name="account_note_saved">Saved!</string>
|
||||||
|
|
||||||
<string name="wellbeing_mode_notice">Some information that might affect your mental wellbeing will be hidden. This includes:\n\n
|
<string name="wellbeing_mode_notice">Some information that might affect your mental wellbeing will be hidden. This includes:\n\n
|
||||||
- Favorite/Boost/Follow notifications\n
|
- Favorite/Boost/Follow notifications\n
|
||||||
- Favorite/Boost count on toots\n
|
- Favorite/Boost count on toots\n
|
||||||
|
@ -598,4 +599,15 @@
|
||||||
<string name="error_upload_max_media_reached">You cannot upload more than %1$d media attachments.</string>
|
<string name="error_upload_max_media_reached">You cannot upload more than %1$d media attachments.</string>
|
||||||
<string name="dialog_delete_list_warning">Do you really want to delete the list %s?</string>
|
<string name="dialog_delete_list_warning">Do you really want to delete the list %s?</string>
|
||||||
|
|
||||||
|
<string name="drafts_toot_failed_to_send">This toot failed to send!</string>
|
||||||
|
|
||||||
|
<string name="new_drafts_warning">
|
||||||
|
The draft feature in Tusky has been completely redesigned to be faster, more user friendly and less buggy.\n
|
||||||
|
You can still access your old drafts via a button on the new drafts screen,
|
||||||
|
but they will be removed in a future update!
|
||||||
|
</string>
|
||||||
|
<string name="old_drafts">Old Drafts</string>
|
||||||
|
<string name="drafts_failed_loading_reply">Failed loading Reply information</string>
|
||||||
|
<string name="draft_deleted">Draft deleted</string>
|
||||||
|
<string name="drafts_toot_reply_removed">The Toot you drafted a reply to has been removed</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -13,7 +13,6 @@
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
* see <http://www.gnu.org/licenses>. */
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
@ -25,6 +24,7 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity
|
||||||
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
import com.keylesspalace.tusky.components.compose.ComposeViewModel
|
||||||
import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT
|
import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT
|
||||||
import com.keylesspalace.tusky.components.compose.MediaUploader
|
import com.keylesspalace.tusky.components.compose.MediaUploader
|
||||||
|
import com.keylesspalace.tusky.components.drafts.DraftHelper
|
||||||
import com.keylesspalace.tusky.db.*
|
import com.keylesspalace.tusky.db.*
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
@ -115,6 +115,7 @@ class ComposeActivityTest {
|
||||||
accountManagerMock,
|
accountManagerMock,
|
||||||
mock(MediaUploader::class.java),
|
mock(MediaUploader::class.java),
|
||||||
mock(ServiceClient::class.java),
|
mock(ServiceClient::class.java),
|
||||||
|
mock(DraftHelper::class.java),
|
||||||
mock(SaveTootHelper::class.java),
|
mock(SaveTootHelper::class.java),
|
||||||
dbMock
|
dbMock
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in a new issue