diff --git a/app/build.gradle b/app/build.gradle index 406b5fa2..68f9c8bb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { applicationId APP_ID minSdkVersion 21 targetSdkVersion 29 - versionCode 79 - versionName "13.1-CW1" + versionCode 80 + versionName "14.0-CW1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true @@ -68,6 +68,9 @@ android { androidExtensions { experimental = true } + buildFeatures { + viewBinding true + } testOptions { unitTests { returnDefaultValues = true diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 78b4be88..a05994a1 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -43,6 +43,10 @@ public *; } +-keep enum com.keylesspalace.tusky.db.DraftAttachment$Type { + public *; +} + # preserve line numbers for crash reporting -keepattributes SourceFile,LineNumberTable -renamesourcefileattribute SourceFile diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json new file mode 100644 index 00000000..c47a305b --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json @@ -0,0 +1,747 @@ +{ + "formatVersion": 1, + "database": { + "version": 24, + "identityHash": "ea8559bbdf434c7b9086384a9a4cc8e6", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `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, 'ea8559bbdf434c7b9086384a9a4cc8e6')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json new file mode 100644 index 00000000..01a491b4 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json @@ -0,0 +1,821 @@ +{ + "formatVersion": 1, + "database": { + "version": 25, + "identityHash": "e2cb844862443c2c5cc884c11f120d43", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e2cb844862443c2c5cc884c11f120d43')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 168990cc..1302a031 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -146,6 +146,7 @@ + { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - } private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) { diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt index bc4c336e..14c5737e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActivity.kt @@ -57,6 +57,7 @@ import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.pager.AccountPagerAdapter +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.view.showMuteAccountDialog import com.keylesspalace.tusky.viewmodel.AccountViewModel @@ -77,16 +78,18 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private val viewModel: AccountViewModel by viewModels { viewModelFactory } - private val accountFieldAdapter = AccountFieldAdapter(this) + private lateinit var accountFieldAdapter : AccountFieldAdapter private var followState: FollowState = FollowState.NOT_FOLLOWING private var blocking: Boolean = false private var muting: Boolean = false private var blockingDomain: Boolean = false private var showingReblogs: Boolean = false + private var subscribing: Boolean = false private var loadedAccount: Account? = null private var animateAvatar: Boolean = false + private var animateEmojis: Boolean = false // fields for scroll animation private var hideFab: Boolean = false @@ -116,12 +119,13 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI loadResources() makeNotificationBarTransparent() setContentView(R.layout.activity_account) - + // Obtain information to fill out the profile. viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!) val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this) animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false) + animateEmojis = sharedPrefs.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) hideFab = sharedPrefs.getBoolean("fabHide", false) setupToolbar() @@ -159,8 +163,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI accountMuteButton.hide() accountFollowsYouTextView.hide() - // setup the RecyclerView for the account fields + accountFieldAdapter = AccountFieldAdapter(this, animateEmojis) accountFieldList.isNestedScrollingEnabled = false accountFieldList.layoutManager = LinearLayoutManager(this) accountFieldList.adapter = accountFieldAdapter @@ -186,6 +190,16 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300) } + // If wellbeing mode is enabled, follow stats and posts count should be hidden + val preferences = PreferenceManager.getDefaultSharedPreferences(this) + val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false) + + if (wellbeingEnabled) { + accountStatuses.hide() + accountFollowers.hide() + accountFollowing.hide() + } + } /** @@ -200,8 +214,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI val pageTitles = arrayOf(getString(R.string.title_statuses), getString(R.string.title_statuses_with_replies), getString(R.string.title_statuses_pinned), getString(R.string.title_media)) - TabLayoutMediator(accountTabLayout, accountFragmentViewPager) { - tab, position -> + TabLayoutMediator(accountTabLayout, accountFragmentViewPager) { tab, position -> tab.text = pageTitles[position] }.attach() @@ -365,16 +378,15 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI val usernameFormatted = getString(R.string.status_username_format, account.username) accountUsernameTextView.text = usernameFormatted - accountDisplayNameTextView.text = account.name.emojify(account.emojis, accountDisplayNameTextView) + accountDisplayNameTextView.text = account.name.emojify(account.emojis, accountDisplayNameTextView, animateEmojis) - val emojifiedNote = account.note.emojify(account.emojis, accountNoteTextView) + val emojifiedNote = account.note.emojify(account.emojis, accountNoteTextView, animateEmojis) LinkHelper.setClickableText(accountNoteTextView, emojifiedNote, null, this) // accountFieldAdapter.fields = account.fields ?: emptyList() accountFieldAdapter.emojis = account.emojis ?: emptyList() accountFieldAdapter.notifyDataSetChanged() - accountLockedImageView.visible(account.locked) accountBadgeTextView.visible(account.bot) @@ -428,7 +440,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI private fun updateToolbar() { loadedAccount?.let { account -> - val emojifiedName = account.name.emojify(account.emojis, accountToolbar) + val emojifiedName = account.name.emojify(account.emojis, accountToolbar, animateEmojis) try { supportActionBar?.title = EmojiCompat.get().process(emojifiedName) @@ -536,13 +548,32 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI blockingDomain = relation.blockingDomain showingReblogs = relation.showingReblogs - accountFollowsYouTextView.visible(relation.followedBy) + // If wellbeing mode is enabled, "follows you" text should not be visible + val preferences = PreferenceManager.getDefaultSharedPreferences(this) + val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false) + + accountFollowsYouTextView.visible(relation.followedBy && !wellbeingEnabled) + + // because subscribing is Pleroma extension, enable it __only__ when we have non-null subscribing field + // it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call + if(!viewModel.isSelf && followState == FollowState.FOLLOWING + && (relation.subscribing != null || relation.notifying != null)) { + accountSubscribeButton.show() + accountSubscribeButton.setOnClickListener { + viewModel.changeSubscribingState() + } + if(relation.notifying != null) + subscribing = relation.notifying + else if(relation.subscribing != null) + subscribing = relation.subscribing + } + + // remove the listener so it doesn't fire on non-user changes + accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher) accountNoteTextInputLayout.visible(relation.note != null) accountNoteTextInputLayout.editText?.setText(relation.note) - // add the listener late to avoid it firing on the first change - accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher) accountNoteTextInputLayout.editText?.addTextChangedListener(noteWatcher) updateButtons() @@ -574,6 +605,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI accountFollowButton.setText(R.string.action_unfollow) } } + updateSubscribeButton() } private fun updateMuteButton() { @@ -584,6 +616,20 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } } + private fun updateSubscribeButton() { + if(followState != FollowState.FOLLOWING) { + accountSubscribeButton.hide() + } + + if(subscribing) { + accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp) + accountSubscribeButton.contentDescription = getString(R.string.action_unsubscribe_account) + } else { + accountSubscribeButton.setIconResource(R.drawable.ic_notifications_24dp) + accountSubscribeButton.contentDescription = getString(R.string.action_subscribe_account) + } + } + private fun updateButtons() { invalidateOptionsMenu() @@ -595,6 +641,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI if (blocking || viewModel.isSelf) { accountFloatingActionButton.hide() accountMuteButton.hide() + accountSubscribeButton.hide() } else { accountFloatingActionButton.show() if (muting) @@ -608,6 +655,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI accountFloatingActionButton.hide() accountFollowButton.hide() accountMuteButton.hide() + accountSubscribeButton.hide() } } @@ -615,14 +663,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI menuInflater.inflate(R.menu.account_toolbar, menu) if (!viewModel.isSelf) { - val follow = menu.findItem(R.id.action_follow) - follow.title = if (followState == FollowState.NOT_FOLLOWING) { - getString(R.string.action_follow) - } else { - getString(R.string.action_unfollow) - } - - follow.isVisible = followState != FollowState.REQUESTED val block = menu.findItem(R.id.action_block) block.title = if (blocking) { @@ -666,8 +706,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } } else { - // It shouldn't be possible to block, follow, mute or report yourself. - menu.removeItem(R.id.action_follow) + // It shouldn't be possible to block, mute or report yourself. menu.removeItem(R.id.action_block) menu.removeItem(R.id.action_mute) menu.removeItem(R.id.action_mute_domain) @@ -722,10 +761,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI if (viewModel.relationshipData.value?.data?.muting != true) { loadedAccount?.let { showMuteAccountDialog( - this, - it.username - ) { notifications -> - viewModel.muteAccount(notifications) + this, + it.username + ) { notifications, duration -> + viewModel.muteAccount(notifications, duration) } } } else { @@ -759,14 +798,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - R.id.action_mention -> { - mention() - return true - } R.id.action_open_in_web -> { // If the account isn't loaded yet, eat the input. if (loadedAccount != null) { @@ -774,10 +805,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } return true } - R.id.action_follow -> { - viewModel.changeFollowState() - return true - } R.id.action_block -> { toggleBlock() return true diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt index 6cf3367e..d592f053 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt @@ -18,7 +18,6 @@ package com.keylesspalace.tusky import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.MenuItem import com.keylesspalace.tusky.fragment.AccountListFragment import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector @@ -68,16 +67,6 @@ class AccountListActivity : BaseActivity(), HasAndroidInjector { .commit() } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - override fun androidInjector() = dispatchingAndroidInjector companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt index 2933d689..f1c3d54d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt @@ -31,6 +31,7 @@ import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel import com.keylesspalace.tusky.viewmodel.State @@ -71,7 +72,9 @@ class AccountsInListFragment : DialogFragment(), Injectable { private val searchAdapter = SearchAdapter() private val radius by lazy { resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) } - private val animateAvatar by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean("animateGifAvatars", false) } + private val pm by lazy { PreferenceManager.getDefaultSharedPreferences(requireContext()) } + private val animateAvatar by lazy { pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) } + private val animateEmojis by lazy { pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -209,7 +212,7 @@ class AccountsInListFragment : DialogFragment(), Injectable { } fun bind(account: Account) { - displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView) + displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis) usernameTextView.text = account.username loadAvatar(account.avatar, avatar, radius, animateAvatar) } @@ -252,7 +255,7 @@ class AccountsInListFragment : DialogFragment(), Injectable { override val containerView = itemView fun bind(account: Account, inAList: Boolean) { - displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView) + displayNameTextView.text = account.name.emojify(account.emojis, displayNameTextView, animateEmojis) usernameTextView.text = account.username loadAvatar(account.avatar, avatar, radius, animateAvatar) diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java index 36387268..92994f16 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -24,6 +24,7 @@ import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.Bundle; import android.util.Log; +import android.view.MenuItem; import android.view.View; import androidx.annotation.NonNull; @@ -127,6 +128,15 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left); } + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } + @Override public void finish() { super.finish(); diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt index 3d86b6e1..64d952b9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -296,10 +296,6 @@ class EditProfileActivity : BaseActivity(), Injectable { override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } R.id.action_save -> { save() return true diff --git a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt index 5adce8ed..0726b26e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt @@ -1,7 +1,6 @@ package com.keylesspalace.tusky import android.os.Bundle -import android.view.MenuItem import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.Toast @@ -205,14 +204,4 @@ class FiltersActivity: BaseActivity() { } } - // Activate back arrow in toolbar - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt index 915baf96..d6cc7bca 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt @@ -18,7 +18,6 @@ package com.keylesspalace.tusky import android.os.Bundle import androidx.annotation.RawRes import android.util.Log -import android.view.MenuItem import android.widget.TextView import com.keylesspalace.tusky.util.IOUtils import kotlinx.android.extensions.CacheImplementation @@ -48,16 +47,6 @@ class LicenseActivity : BaseActivity() { } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) { val sb = StringBuilder() diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt index 3894f652..fa3c92c3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -21,7 +21,6 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.LayoutInflater -import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.* @@ -130,19 +129,27 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { else R.string.action_rename_list) { _, _ -> onPickedDialogName(editText.text, list?.id) } - .setNegativeButton(android.R.string.cancel) { d, _ -> - d.dismiss() - } + .setNegativeButton(android.R.string.cancel, null) .show() val positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE) editText.onTextChanged { s, _, _, _ -> - positiveButton.isEnabled = !s.isBlank() + positiveButton.isEnabled = s.isNotBlank() } editText.setText(list?.title) editText.text?.let { editText.setSelection(it.length) } } + private fun showListDeleteDialog(list: MastoList) { + AlertDialog.Builder(this) + .setMessage(getString(R.string.dialog_delete_list_warning, list.title)) + .setPositiveButton(R.string.action_delete){ _, _ -> + viewModel.deleteList(list.id) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + private fun update(state: ListsViewModel.State) { adapter.submitList(state.lists) @@ -199,7 +206,7 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { when (item.itemId) { R.id.list_edit -> openListSettings(list) R.id.list_rename -> renameListDialog(list) - R.id.list_delete -> viewModel.deleteList(list.id) + R.id.list_delete -> showListDeleteDialog(list) else -> return@setOnMenuItemClickListener false } true @@ -210,14 +217,6 @@ class ListsActivity : BaseActivity(), Injectable, HasAndroidInjector { override fun androidInjector() = dispatchingAndroidInjector - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - onBackPressed() - return true - } - return false - } - private object ListsDiffer : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: MastoList, newItem: MastoList): Boolean { return oldItem.id == newItem.id diff --git a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt index 2269d328..d2911ec0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/LoginActivity.kt @@ -20,14 +20,13 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.net.Uri -import android.os.Build import android.os.Bundle import android.text.method.LinkMovementMethod import android.util.Log -import android.view.MenuItem import android.view.View import android.widget.TextView import androidx.appcompat.app.AlertDialog +import androidx.browser.customtabs.CustomTabColorSchemeParams import androidx.browser.customtabs.CustomTabsIntent import com.bumptech.glide.Glide import com.keylesspalace.tusky.di.Injectable @@ -362,16 +361,19 @@ class LoginActivity : BaseActivity(), Injectable { private fun openInCustomTab(uri: Uri, context: Context): Boolean { val toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface) - val customTabsIntentBuilder = CustomTabsIntent.Builder() + val navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor) + val navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor) + + val colorSchemeParams = CustomTabColorSchemeParams.Builder() .setToolbarColor(toolbarColor) + .setNavigationBarColor(navigationbarColor) + .setNavigationBarDividerColor(navigationbarDividerColor) + .build() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - customTabsIntentBuilder.setNavigationBarColor( - ThemeUtils.getColor(context, android.R.attr.navigationBarColor) - ) - } + val customTabsIntent = CustomTabsIntent.Builder() + .setDefaultColorSchemeParams(colorSchemeParams) + .build() - val customTabsIntent = customTabsIntentBuilder.build() try { customTabsIntent.launchUrl(context, uri) } catch (e: ActivityNotFoundException) { diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 31a4e80c..3b3af8ae 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -31,6 +31,7 @@ import android.widget.ImageView import androidx.appcompat.app.AlertDialog import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat +import androidx.core.content.edit import androidx.core.content.pm.ShortcutManagerCompat import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat.InitCallback @@ -52,11 +53,14 @@ import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType import com.keylesspalace.tusky.components.conversation.ConversationsRepository +import com.keylesspalace.tusky.components.drafts.DraftHelper +import com.keylesspalace.tusky.components.drafts.DraftsActivity import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.components.preference.PreferencesActivity import com.keylesspalace.tusky.components.scheduled.ScheduledTootActivity import com.keylesspalace.tusky.components.search.SearchActivity import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.interfaces.AccountSelectionListener @@ -98,6 +102,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje @Inject lateinit var conversationRepository: ConversationsRepository + @Inject + lateinit var appDb: AppDatabase + + @Inject + lateinit var draftHelper: DraftHelper + private lateinit var header: AccountHeaderView private var notificationTabPosition = 0 @@ -229,6 +239,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje // Flush old media that was cached for sharing deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky")) } + draftWarning() } override fun onResume() { @@ -397,7 +408,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje nameRes = R.string.action_access_saved_toot iconRes = R.drawable.ic_notebook onClick = { - val intent = Intent(context, SavedTootActivity::class.java) + val intent = DraftsActivity.newIntent(context) startActivityWithSlideInAnimation(intent) } }, @@ -554,6 +565,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje val activeTabPosition = if (selectNotificationTab) notificationTabPosition else 0 mainToolbar.title = tabs[activeTabPosition].title(this@MainActivity) + mainToolbar.setOnClickListener { + (adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect() + } } @@ -601,6 +615,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this) cacheUpdater.clearForUser(activeAccount.id) conversationRepository.deleteCacheForAccount(activeAccount.id) + draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id) removeShortcut(this, activeAccount) val newAccount = accountManager.logActiveAccountOut() if (!NotificationHelper.areNotificationsEnabled(this, accountManager)) { @@ -679,16 +694,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje .into(object : CustomTarget(navIconSize, navIconSize) { override fun onLoadStarted(placeholder: Drawable?) { - if(placeholder != null) { + if (placeholder != null) { mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) } } override fun onResourceReady(resource: Drawable, transition: Transition?) { - mainToolbar.navigationIcon = resource + mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize) } override fun onLoadCleared(placeholder: Drawable?) { - mainToolbar.navigationIcon = placeholder + if (placeholder != null) { + mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize) + } } }) } @@ -713,8 +730,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } private fun updateProfiles() { + val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) val profiles: MutableList = accountManager.getAllAccountsOrderedByActive().map { acc -> - val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header)) + val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis)) ProfileDrawerItem().apply { isSelected = acc.isActive @@ -738,6 +756,29 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje 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 androidInjector() = androidInjector diff --git a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt index e4655a5e..c3017b0c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ModalTimelineActivity.kt @@ -3,7 +3,6 @@ package com.keylesspalace.tusky import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.MenuItem import com.google.android.material.floatingactionbutton.FloatingActionButton import com.keylesspalace.tusky.fragment.TimelineFragment import com.keylesspalace.tusky.interfaces.ActionButtonActivity @@ -56,14 +55,6 @@ class ModalTimelineActivity : BottomSheetActivity(), ActionButtonActivity, HasAn override fun getActionButton(): FloatingActionButton? = null - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - onBackPressed() - return true - } - return false - } - override fun androidInjector() = dispatchingAndroidInjector } diff --git a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java index 9a163989..63a32b17 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/SavedTootActivity.java @@ -18,7 +18,6 @@ package com.keylesspalace.tusky; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; -import android.view.MenuItem; import android.view.View; import androidx.annotation.Nullable; @@ -89,7 +88,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd setSupportActionBar(toolbar); ActionBar bar = getSupportActionBar(); if (bar != null) { - bar.setTitle(getString(R.string.title_saved_toot)); + bar.setTitle(getString(R.string.title_drafts)); bar.setDisplayHomeAsUpEnabled(true); bar.setDisplayShowHomeEnabled(true); } @@ -118,17 +117,6 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd if (asyncTask != null) asyncTask.cancel(true); } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: { - onBackPressed(); - return true; - } - } - return super.onOptionsItemSelected(item); - } - private void fetchToots() { asyncTask = new FetchPojosTask(this, database.tootDao()) .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); @@ -166,6 +154,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd ComposeOptions composeOptions = new ComposeOptions( /*scheduledTootUid*/null, item.getUid(), + /*drafId*/null, item.getText(), jsonUrls, descriptions, @@ -177,6 +166,7 @@ public final class SavedTootActivity extends BaseActivity implements SavedTootAd item.getInReplyToUsername(), item.getInReplyToText(), /*mediaAttachments*/null, + /*draftAttachments*/null, /*scheduledAt*/null, /*sensitive*/null, /*poll*/null, diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt index 56ea4d2f..9eba5bbe 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt @@ -18,7 +18,6 @@ package com.keylesspalace.tusky import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.MenuItem import androidx.fragment.app.commit import com.keylesspalace.tusky.fragment.TimelineFragment @@ -66,14 +65,6 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home){ - onBackPressed() - return true - } - return super.onOptionsItemSelected(item) - } - override fun androidInjector() = dispatchingAndroidInjector companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt index 6b1aaef9..2b61f141 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt @@ -18,7 +18,6 @@ package com.keylesspalace.tusky import android.graphics.Color import android.os.Bundle import android.util.Log -import android.view.MenuItem import android.view.View import android.widget.FrameLayout import androidx.appcompat.app.AlertDialog @@ -345,14 +344,6 @@ class TabPreferenceActivity : BaseActivity(), Injectable, ItemInteractionListene } } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - if (item.itemId == android.R.id.home) { - onBackPressed() - return true - } - return false - } - override fun onPause() { super.onPause() if (tabsChanged) { diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java index a49dcc88..0ff6ff56 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewTagActivity.java @@ -18,7 +18,6 @@ package com.keylesspalace.tusky; import android.content.Context; import android.content.Intent; import android.os.Bundle; -import android.view.MenuItem; import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; @@ -72,17 +71,6 @@ public class ViewTagActivity extends BottomSheetActivity implements HasAndroidIn fragmentTransaction.commit(); } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: { - onBackPressed(); - return true; - } - } - return super.onOptionsItemSelected(item); - } - @Override public AndroidInjector androidInjector() { return dispatchingAndroidInjector; diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java b/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java index e2ae63d1..88fb88cc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java @@ -110,10 +110,6 @@ public class ViewThreadActivity extends BottomSheetActivity implements HasAndroi @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { - case android.R.id.home: { - onBackPressed(); - return true; - } case R.id.action_open_in_web: { LinkHelper.openLink(getIntent().getStringExtra(URL_EXTRA), this); return true; diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java index 5c52e39e..24430dce 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountAdapter.java @@ -33,10 +33,14 @@ public abstract class AccountAdapter extends RecyclerView.Adapter { List accountList; AccountActionListener accountActionListener; private boolean bottomLoading; + protected final boolean animateEmojis; + protected final boolean animateAvatar; - AccountAdapter(AccountActionListener accountActionListener) { + AccountAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) { this.accountList = new ArrayList<>(); this.accountActionListener = accountActionListener; + this.animateAvatar = animateAvatar; + this.animateEmojis = animateEmojis; bottomLoading = false; } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt index e80129c6..e395a7e6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldAdapter.kt @@ -29,7 +29,7 @@ import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.util.* import kotlinx.android.synthetic.main.item_account_field.view.* -class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView.Adapter() { +class AccountFieldAdapter(private val linkListener: LinkListener, private val animateEmojis: Boolean) : RecyclerView.Adapter() { var emojis: List = emptyList() var fields: List> = emptyList() @@ -55,10 +55,10 @@ class AccountFieldAdapter(private val linkListener: LinkListener) : RecyclerView viewHolder.valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) } else { val field = proofOrField.asRight() - val emojifiedName = field.name.emojify(emojis, viewHolder.nameTextView) + val emojifiedName = field.name.emojify(emojis, viewHolder.nameTextView, animateEmojis) viewHolder.nameTextView.text = emojifiedName - val emojifiedValue = field.value.emojify(emojis, viewHolder.valueTextView) + val emojifiedValue = field.value.emojify(emojis, viewHolder.valueTextView, animateEmojis) LinkHelper.setClickableText(viewHolder.valueTextView, emojifiedValue, null, linkListener) if(field.verifiedAt != null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt index dae0db4b..c8df79f9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt @@ -23,6 +23,7 @@ import android.widget.ArrayAdapter import androidx.preference.PreferenceManager import com.keylesspalace.tusky.R import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.* import kotlinx.android.synthetic.main.item_autocomplete_account.view.* @@ -41,12 +42,14 @@ class AccountSelectionAdapter(context: Context) : ArrayAdapter(co val username = view.username val displayName = view.display_name val avatar = view.avatar + val pm = PreferenceManager.getDefaultSharedPreferences(avatar.context) + val animateEmojis = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + username.text = account.fullName - displayName.text = account.displayName.emojify(account.emojis, displayName) + displayName.text = account.displayName.emojify(account.emojis, displayName, animateEmojis) val avatarRadius = avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp) - val animateAvatar = PreferenceManager.getDefaultSharedPreferences(avatar.context) - .getBoolean("animateGifAvatars", false) + val animateAvatar = pm.getBoolean("animateGifAvatars", false) loadAvatar(account.profilePictureUrl, avatar, avatarRadius, animateAvatar) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java index 7b07d5bd..559426e3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java @@ -22,7 +22,6 @@ public class AccountViewHolder extends RecyclerView.ViewHolder { private ImageView avatarInset; private String accountId; private boolean showBotOverlay; - private boolean animateAvatar; public AccountViewHolder(View itemView) { super(itemView); @@ -32,15 +31,14 @@ public class AccountViewHolder extends RecyclerView.ViewHolder { avatarInset = itemView.findViewById(R.id.account_avatar_inset); SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()); showBotOverlay = sharedPrefs.getBoolean("showBotOverlay", true); - animateAvatar = sharedPrefs.getBoolean("animateGifAvatars", false); } - public void setupWithAccount(Account account) { + public void setupWithAccount(Account account, boolean animateAvatar, boolean animateEmojis) { accountId = account.getId(); String format = username.getContext().getString(R.string.status_username_format); String formattedUsername = String.format(format, account.getUsername()); username.setText(formattedUsername); - CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); + CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis); displayName.setText(emojifiedName); int avatarRadius = avatar.getContext().getResources() .getDimensionPixelSize(R.dimen.avatar_radius_48dp); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java index 073d76da..13144cb8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/BlocksAdapter.java @@ -34,8 +34,8 @@ import com.keylesspalace.tusky.util.ImageLoadingHelper; public class BlocksAdapter extends AccountAdapter { - public BlocksAdapter(AccountActionListener accountActionListener) { - super(accountActionListener); + public BlocksAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) { + super(accountActionListener, animateAvatar, animateEmojis); } @NonNull @@ -60,7 +60,7 @@ public class BlocksAdapter extends AccountAdapter { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder; - holder.setupWithAccount(accountList.get(position)); + holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis); holder.setupActionListener(accountActionListener); } } @@ -71,7 +71,6 @@ public class BlocksAdapter extends AccountAdapter { private TextView displayName; private ImageButton unblock; private String id; - private boolean animateAvatar; BlockedUserViewHolder(View itemView) { super(itemView); @@ -79,14 +78,12 @@ public class BlocksAdapter extends AccountAdapter { username = itemView.findViewById(R.id.blocked_user_username); displayName = itemView.findViewById(R.id.blocked_user_display_name); unblock = itemView.findViewById(R.id.blocked_user_unblock); - animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()) - .getBoolean("animateGifAvatars", false); } - void setupWithAccount(Account account) { + void setupWithAccount(Account account, boolean animateAvatar, boolean animateEmojis) { id = account.getId(); - CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); + CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis); displayName.setText(emojifiedName); String format = username.getContext().getString(R.string.status_username_format); String formattedUsername = String.format(format, account.getUsername()); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java index 82158746..98cb9e4d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowAdapter.java @@ -27,8 +27,8 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener; /** Both for follows and following lists. */ public class FollowAdapter extends AccountAdapter { - public FollowAdapter(AccountActionListener accountActionListener) { - super(accountActionListener); + public FollowAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) { + super(accountActionListener, animateAvatar, animateEmojis); } @NonNull @@ -53,7 +53,7 @@ public class FollowAdapter extends AccountAdapter { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { AccountViewHolder holder = (AccountViewHolder) viewHolder; - holder.setupWithAccount(accountList.get(position)); + holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis); holder.setupActionListener(accountActionListener); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt index dec4586b..8fa14731 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt @@ -10,27 +10,24 @@ import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.interfaces.AccountActionListener -import com.keylesspalace.tusky.util.emojify -import com.keylesspalace.tusky.util.loadAvatar -import com.keylesspalace.tusky.util.unicodeWrap -import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.util.* import kotlinx.android.synthetic.main.item_follow_request_notification.view.* -internal class FollowRequestViewHolder(itemView: View, private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) { +internal class FollowRequestViewHolder( + itemView: View, + private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) { private var id: String? = null - private val animateAvatar: Boolean = PreferenceManager.getDefaultSharedPreferences(itemView.context) - .getBoolean("animateGifAvatars", false) - fun setupWithAccount(account: Account) { + fun setupWithAccount(account: Account, animateAvatar: Boolean, animateEmojis: Boolean) { id = account.id val wrappedName = account.name.unicodeWrap() - val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView) + val emojifiedName: CharSequence = wrappedName.emojify(account.emojis, itemView, animateEmojis) itemView.displayNameTextView.text = emojifiedName if (showHeader) { val wholeMessage: String = itemView.context.getString(R.string.notification_follow_request_format, wrappedName) itemView.notificationTextView?.text = SpannableStringBuilder(wholeMessage).apply { setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - }.emojify(account.emojis, itemView) + }.emojify(account.emojis, itemView, animateEmojis) } itemView.notificationTextView?.visible(showHeader) val format = itemView.context.getString(R.string.status_username_format) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java index dab3d4fe..9ba59884 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java @@ -27,8 +27,8 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener; public class FollowRequestsAdapter extends AccountAdapter { - public FollowRequestsAdapter(AccountActionListener accountActionListener) { - super(accountActionListener); + public FollowRequestsAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) { + super(accountActionListener, animateAvatar, animateEmojis); } @NonNull @@ -53,7 +53,7 @@ public class FollowRequestsAdapter extends AccountAdapter { public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; - holder.setupWithAccount(accountList.get(position)); + holder.setupWithAccount(accountList.get(position), animateAvatar, animateEmojis); holder.setupActionListener(accountActionListener); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java index c4224c9c..e1a30759 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/MutesAdapter.java @@ -23,8 +23,8 @@ import java.util.HashMap; public class MutesAdapter extends AccountAdapter { private HashMap mutingNotificationsMap; - public MutesAdapter(AccountActionListener accountActionListener) { - super(accountActionListener); + public MutesAdapter(AccountActionListener accountActionListener, boolean animateAvatar, boolean animateEmojis) { + super(accountActionListener, animateAvatar, animateEmojis); mutingNotificationsMap = new HashMap(); } @@ -51,7 +51,7 @@ public class MutesAdapter extends AccountAdapter { if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder; Account account = accountList.get(position); - holder.setupWithAccount(account, mutingNotificationsMap.get(account.getId())); + holder.setupWithAccount(account, mutingNotificationsMap.get(account.getId()), animateAvatar, animateEmojis); holder.setupActionListener(accountActionListener); } } @@ -73,7 +73,6 @@ public class MutesAdapter extends AccountAdapter { private ImageButton unmute; private ImageButton muteNotifications; private String id; - private boolean animateAvatar; private boolean notifications; MutedUserViewHolder(View itemView) { @@ -83,13 +82,11 @@ public class MutesAdapter extends AccountAdapter { displayName = itemView.findViewById(R.id.muted_user_display_name); unmute = itemView.findViewById(R.id.muted_user_unmute); muteNotifications = itemView.findViewById(R.id.muted_user_mute_notifications); - animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext()) - .getBoolean("animateGifAvatars", false); } - void setupWithAccount(Account account, Boolean mutingNotifications) { + void setupWithAccount(Account account, Boolean mutingNotifications, boolean animateAvatar, boolean animateEmojis) { id = account.getId(); - CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName); + CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), account.getEmojis(), displayName, animateEmojis); displayName.setText(emojifiedName); String format = username.getContext().getString(R.string.status_username_format); String formattedUsername = String.format(format, account.getUsername()); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 6105febb..314aed5c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -37,6 +37,7 @@ import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.RecyclerView; +import com.bumptech.glide.Glide; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.entity.Account; import com.keylesspalace.tusky.entity.Emoji; @@ -198,8 +199,12 @@ public class NotificationsAdapter extends RecyclerView.Adapter { holder.setUsername(statusViewData.getNickname()); holder.setCreatedAt(statusViewData.getCreatedAt()); - holder.setAvatars(concreteNotificaton.getStatusViewData().getAvatar(), - concreteNotificaton.getAccount().getAvatar()); + if(concreteNotificaton.getType() == Notification.Type.STATUS) { + holder.setAvatar(statusViewData.getAvatar(), statusViewData.isBot()); + } else { + holder.setAvatars(statusViewData.getAvatar(), + concreteNotificaton.getAccount().getAvatar()); + } } holder.setMessage(concreteNotificaton, statusListener); @@ -227,7 +232,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { case VIEW_TYPE_FOLLOW_REQUEST: { if (payloadForHolder == null) { FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder; - holder.setupWithAccount(concreteNotificaton.getAccount()); + holder.setupWithAccount(concreteNotificaton.getAccount(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis()); holder.setupActionListener(accountActionListener); } } @@ -249,7 +254,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter { statusDisplayOptions.showBotOverlay(), statusDisplayOptions.useBlurhash(), CardViewMode.NONE, - statusDisplayOptions.confirmReblogs() + statusDisplayOptions.confirmReblogs(), + statusDisplayOptions.hideStats(), + statusDisplayOptions.animateEmojis() ); } @@ -267,6 +274,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { case POLL: { return VIEW_TYPE_STATUS; } + case STATUS: case FAVOURITE: case REBLOG: { return VIEW_TYPE_STATUS_NOTIFICATION; @@ -329,13 +337,17 @@ public class NotificationsAdapter extends RecyclerView.Adapter { String format = context.getString(R.string.notification_follow_format); String wrappedDisplayName = StringUtils.unicodeWrap(account.getName()); String wholeMessage = String.format(format, wrappedDisplayName); - CharSequence emojifiedMessage = CustomEmojiHelper.emojify(wholeMessage, account.getEmojis(), message); + CharSequence emojifiedMessage = CustomEmojiHelper.emojify( + wholeMessage, account.getEmojis(), message, statusDisplayOptions.animateEmojis() + ); message.setText(emojifiedMessage); String username = context.getString(R.string.status_username_format, account.getUsername()); usernameView.setText(username); - CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify(wrappedDisplayName, account.getEmojis(), usernameView); + CharSequence emojifiedDisplayName = CustomEmojiHelper.emojify( + wrappedDisplayName, account.getEmojis(), usernameView, statusDisplayOptions.animateEmojis() + ); displayNameView.setText(emojifiedDisplayName); @@ -373,6 +385,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter { private StatusViewData.Concrete statusViewData; private SimpleDateFormat shortSdf; private SimpleDateFormat longSdf; + + private int avatarRadius48dp; + private int avatarRadius36dp; + private int avatarRadius24dp; StatusNotificationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) { super(itemView); @@ -398,6 +414,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter { statusContent.setOnClickListener(this); shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()); + + this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp); + this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp); + this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp); } private void showNotificationContent(boolean show) { @@ -410,7 +430,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } private void setDisplayName(String name, List emojis) { - CharSequence emojifiedName = CustomEmojiHelper.emojify(name, emojis, displayName); + CharSequence emojifiedName = CustomEmojiHelper.emojify(name, emojis, displayName, statusDisplayOptions.animateEmojis()); displayName.setText(emojifiedName); } @@ -488,13 +508,25 @@ public class NotificationsAdapter extends RecyclerView.Adapter { format = context.getString(R.string.notification_reblog_format); break; } + case STATUS: { + icon = ContextCompat.getDrawable(context, R.drawable.ic_home_24dp); + if (icon != null) { + icon.setColorFilter(ContextCompat.getColor(context, + R.color.tusky_blue), PorterDuff.Mode.SRC_ATOP); + } + + format = context.getString(R.string.notification_subscription_format); + break; + } } message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); String wholeMessage = String.format(format, displayName); final SpannableStringBuilder str = new SpannableStringBuilder(wholeMessage); str.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - CharSequence emojifiedText = CustomEmojiHelper.emojify(str, notificationViewData.getAccount().getEmojis(), message); + CharSequence emojifiedText = CustomEmojiHelper.emojify( + str, notificationViewData.getAccount().getEmojis(), message, statusDisplayOptions.animateEmojis() + ); message.setText(emojifiedText); if (statusViewData != null) { @@ -526,19 +558,34 @@ public class NotificationsAdapter extends RecyclerView.Adapter { this.notificationId = notificationId; } - void setAvatars(@Nullable String statusAvatarUrl, @Nullable String notificationAvatarUrl) { - - int statusAvatarRadius = statusAvatar.getContext().getResources() - .getDimensionPixelSize(R.dimen.avatar_radius_36dp); + void setAvatar(@Nullable String statusAvatarUrl, boolean isBot) { + statusAvatar.setPaddingRelative(0, 0, 0, 0); ImageLoadingHelper.loadAvatar(statusAvatarUrl, - statusAvatar, statusAvatarRadius, statusDisplayOptions.animateAvatars()); + statusAvatar, avatarRadius48dp, statusDisplayOptions.animateAvatars()); - int notificationAvatarRadius = statusAvatar.getContext().getResources() - .getDimensionPixelSize(R.dimen.avatar_radius_24dp); + if (statusDisplayOptions.showBotOverlay() && isBot) { + notificationAvatar.setVisibility(View.VISIBLE); + notificationAvatar.setBackgroundColor(0x50ffffff); + Glide.with(notificationAvatar) + .load(R.drawable.ic_bot_24dp) + .into(notificationAvatar); + } else { + notificationAvatar.setVisibility(View.GONE); + } + } + + void setAvatars(@Nullable String statusAvatarUrl, @Nullable String notificationAvatarUrl) { + int padding = Utils.dpToPx(statusAvatar.getContext(), 12); + statusAvatar.setPaddingRelative(0, 0, padding, padding); + + ImageLoadingHelper.loadAvatar(statusAvatarUrl, + statusAvatar, avatarRadius36dp, statusDisplayOptions.animateAvatars()); + + notificationAvatar.setVisibility(View.VISIBLE); ImageLoadingHelper.loadAvatar(notificationAvatarUrl, notificationAvatar, - notificationAvatarRadius, statusDisplayOptions.animateAvatars()); + avatarRadius24dp, statusDisplayOptions.animateAvatars()); } @Override @@ -590,11 +637,17 @@ public class NotificationsAdapter extends RecyclerView.Adapter { statusContent.setFilters(NO_INPUT_FILTER); } - CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, statusContent); + CharSequence emojifiedText = CustomEmojiHelper.emojify( + content, emojis, statusContent, statusDisplayOptions.animateEmojis() + ); LinkHelper.setClickableText(statusContent, emojifiedText, statusViewData.getMentions(), listener); - CharSequence emojifiedContentWarning = - CustomEmojiHelper.emojify(statusViewData.getSpoilerText(), statusViewData.getStatusEmojis(), contentWarningDescriptionTextView); + CharSequence emojifiedContentWarning = CustomEmojiHelper.emojify( + statusViewData.getSpoilerText(), + statusViewData.getStatusEmojis(), + contentWarningDescriptionTextView, + statusDisplayOptions.animateEmojis() + ); contentWarningDescriptionTextView.setText(emojifiedContentWarning); } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt index a990d326..0208b953 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt @@ -38,6 +38,7 @@ class PollAdapter: RecyclerView.Adapter() { private var mode = RESULT private var emojis: List = emptyList() private var resultClickListener: View.OnClickListener? = null + private var animateEmojis = false fun setup( options: List, @@ -45,13 +46,15 @@ class PollAdapter: RecyclerView.Adapter() { votersCount: Int?, emojis: List, mode: Int, - resultClickListener: View.OnClickListener?) { + resultClickListener: View.OnClickListener?, + animateEmojis: Boolean) { this.pollOptions = options this.voteCount = voteCount this.votersCount = votersCount this.emojis = emojis this.mode = mode this.resultClickListener = resultClickListener + this.animateEmojis = animateEmojis notifyDataSetChanged() } @@ -81,7 +84,7 @@ class PollAdapter: RecyclerView.Adapter() { RESULT -> { val percent = calculatePercent(option.votesCount, votersCount, voteCount) val emojifiedPollOptionText = buildDescription(option.title, percent, holder.resultTextView.context) - .emojify(emojis, holder.resultTextView) + .emojify(emojis, holder.resultTextView, animateEmojis) holder.resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText) val level = percent * 100 @@ -90,7 +93,7 @@ class PollAdapter: RecyclerView.Adapter() { holder.resultTextView.setOnClickListener(resultClickListener) } SINGLE -> { - val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton) + val emojifiedPollOptionText = option.title.emojify(emojis, holder.radioButton, animateEmojis) holder.radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText) holder.radioButton.isChecked = option.selected holder.radioButton.setOnClickListener { @@ -101,7 +104,7 @@ class PollAdapter: RecyclerView.Adapter() { } } MULTIPLE -> { - val emojifiedPollOptionText = option.title.emojify(emojis, holder.checkBox) + val emojifiedPollOptionText = option.title.emojify(emojis, holder.checkBox, animateEmojis) holder.checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText) holder.checkBox.isChecked = option.selected holder.checkBox.setOnCheckedChangeListener { _, isChecked -> diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index 046ab9cf..3fc27d7c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -181,8 +181,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { protected abstract int getMediaPreviewHeight(Context context); - protected void setDisplayName(String name, List customEmojis) { - CharSequence emojifiedName = CustomEmojiHelper.emojify(name, customEmojis, displayName); + protected void setDisplayName(String name, List customEmojis, StatusDisplayOptions statusDisplayOptions) { + CharSequence emojifiedName = CustomEmojiHelper.emojify( + name, customEmojis, displayName, statusDisplayOptions.animateEmojis() + ); displayName.setText(emojifiedName); } @@ -206,7 +208,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { final StatusActionListener listener) { boolean sensitive = !TextUtils.isEmpty(spoilerText); if (sensitive) { - CharSequence emojiSpoiler = CustomEmojiHelper.emojify(spoilerText, emojis, contentWarningDescription); + CharSequence emojiSpoiler = CustomEmojiHelper.emojify( + spoilerText, emojis, contentWarningDescription, statusDisplayOptions.animateEmojis() + ); contentWarningDescription.setText(emojiSpoiler); contentWarningDescription.setVisibility(View.VISIBLE); contentWarningButton.setVisibility(View.VISIBLE); @@ -245,7 +249,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { StatusDisplayOptions statusDisplayOptions, final StatusActionListener listener) { if (expanded) { - CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content); + CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content, statusDisplayOptions.animateEmojis()); LinkHelper.setClickableText(this.content, emojifiedText, mentions, listener); for (int i = 0; i < mediaLabels.length; ++i) { updateMediaLabel(i, sensitive, expanded); @@ -533,7 +537,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { @DrawableRes private static int getLabelIcon(Attachment.Type type) { switch (type) { - default: case IMAGE: return R.drawable.ic_photo_24dp; case GIFV: @@ -541,6 +544,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { return R.drawable.ic_videocam_24dp; case AUDIO: return R.drawable.ic_music_box_24dp; + default: + return R.drawable.ic_attach_file_24dp; } } @@ -708,7 +713,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { StatusDisplayOptions statusDisplayOptions, @Nullable Object payloads) { if (payloads == null) { - setDisplayName(status.getUserFullName(), status.getAccountEmojis()); + setDisplayName(status.getUserFullName(), status.getAccountEmojis(), statusDisplayOptions); setUsername(status.getNickname()); setCreatedAt(status.getCreatedAt(), statusDisplayOptions); setIsReply(status.getInReplyToId() != null); @@ -718,7 +723,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { setBookmarked(status.isBookmarked()); List attachments = status.getAttachments(); boolean sensitive = status.isSensitive(); - if (statusDisplayOptions.mediaPreviewEnabled() && !hasAudioAttachment(attachments)) { + if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash()); if (attachments.size() == 0) { @@ -767,13 +772,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } } - protected static boolean hasAudioAttachment(List attachments) { + protected static boolean hasPreviewableAttachment(List attachments) { for (Attachment attachment : attachments) { - if (attachment.getType() == Attachment.Type.AUDIO) { - return true; + if (attachment.getType() == Attachment.Type.AUDIO || attachment.getType() == Attachment.Type.UNKNOWN) { + return false; } } - return false; + return true; } private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status, @@ -926,12 +931,28 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { listener.onViewThread(position); } }; - pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, PollAdapter.RESULT, viewThreadListener); + pollAdapter.setup( + poll.getOptions(), + poll.getVotesCount(), + poll.getVotersCount(), + emojis, + PollAdapter.RESULT, + viewThreadListener, + statusDisplayOptions.animateEmojis() + ); pollButton.setVisibility(View.GONE); } else { // voting possible - pollAdapter.setup(poll.getOptions(), poll.getVotesCount(), poll.getVotersCount(), emojis, poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE, null); + pollAdapter.setup( + poll.getOptions(), + poll.getVotesCount(), + poll.getVotersCount(), + emojis, + poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE, + null, + statusDisplayOptions.animateEmojis() + ); pollButton.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java index 6350dde0..8755e8e8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -108,7 +108,12 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { super.setupWithStatus(status, listener, statusDisplayOptions, payloads); setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions); // Always show card for detailed status if (payloads == null) { - setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener); + + if (!statusDisplayOptions.hideStats()) { + setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener); + } else { + hideQuantitativeStats(); + } setApplication(status.getApplication()); @@ -174,4 +179,10 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { null ); } + + private void hideQuantitativeStats() { + reblogs.setVisibility(View.GONE); + favourites.setVisibility(View.GONE); + infoDivider.setVisibility(View.GONE); + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java index 56cef6ee..043b7b35 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -27,8 +27,10 @@ import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.SmartLengthInputFilter; import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.util.StringUtils; import com.keylesspalace.tusky.viewdata.StatusViewData; import at.connyduck.sparkbutton.helpers.Utils; @@ -64,7 +66,7 @@ public class StatusViewHolder extends StatusBaseViewHolder { if (rebloggedByDisplayName == null) { hideStatusInfo(); } else { - setRebloggedByDisplayName(rebloggedByDisplayName); + setRebloggedByDisplayName(rebloggedByDisplayName, status, statusDisplayOptions); statusInfo.setOnClickListener(v -> listener.onOpenReblog(getAdapterPosition())); } @@ -73,10 +75,16 @@ public class StatusViewHolder extends StatusBaseViewHolder { } - private void setRebloggedByDisplayName(final String name) { + private void setRebloggedByDisplayName(final CharSequence name, + final StatusViewData.Concrete status, + final StatusDisplayOptions statusDisplayOptions) { Context context = statusInfo.getContext(); - String boostedText = context.getString(R.string.status_boosted_format, name); - statusInfo.setText(boostedText); + CharSequence wrappedName = StringUtils.unicodeWrap(name); + CharSequence boostedText = context.getString(R.string.status_boosted_format, wrappedName); + CharSequence emojifiedText = CustomEmojiHelper.emojify( + boostedText, status.getRebloggedByAccountEmojis(), statusInfo, statusDisplayOptions.animateEmojis() + ); + statusInfo.setText(emojifiedText); statusInfo.setVisibility(View.VISIBLE); } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java index 91ec25e2..4be922d6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TimelineAdapter.java @@ -65,7 +65,9 @@ public final class TimelineAdapter extends RecyclerView.Adapter { statusDisplayOptions.showBotOverlay(), statusDisplayOptions.useBlurhash(), statusDisplayOptions.cardViewMode(), - statusDisplayOptions.confirmReblogs() + statusDisplayOptions.confirmReblogs(), + statusDisplayOptions.hideStats(), + statusDisplayOptions.animateEmojis() ); } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt index 29b57a2e..b54b1555 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt @@ -32,6 +32,7 @@ import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.emojify import kotlinx.android.synthetic.main.item_announcement.view.* + interface AnnouncementActionListener: LinkListener { fun openReactionPicker(announcementId: String, target: View) fun addReaction(announcementId: String, name: String) @@ -40,7 +41,9 @@ interface AnnouncementActionListener: LinkListener { class AnnouncementAdapter( private var items: List = emptyList(), - private val listener: AnnouncementActionListener + private val listener: AnnouncementActionListener, + private val wellbeingEnabled: Boolean = false, + private val animateEmojis: Boolean = false ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AnnouncementViewHolder { @@ -68,6 +71,14 @@ class AnnouncementAdapter( fun bind(item: Announcement) { LinkHelper.setClickableText(text, item.content, null, listener) + // If wellbeing mode is enabled, announcement badge counts should not be shown. + if (wellbeingEnabled) { + // Since reactions are not visible in wellbeing mode, + // we shouldn't be able to add any ourselves. + addReactionChip.visibility = View.GONE + return + } + item.reactions.forEachIndexed { i, reaction -> (chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip? ?: Chip(ContextThemeWrapper(view.context, R.style.Widget_MaterialComponents_Chip_Choice)).apply { @@ -89,7 +100,8 @@ class AnnouncementAdapter( reaction.staticUrl ?: "", null )), - this + this, + animateEmojis ) isChecked = reaction.me diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt index 0adcd012..fe6cf1ba 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt @@ -17,18 +17,22 @@ package com.keylesspalace.tusky.components.announcements import android.content.Context import android.content.Intent +import android.content.SharedPreferences import android.os.Bundle -import android.view.MenuItem import android.view.View import android.widget.PopupWindow import androidx.activity.viewModels +import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager -import com.keylesspalace.tusky.* +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.ViewTagActivity import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.* import com.keylesspalace.tusky.view.EmojiPicker import kotlinx.android.synthetic.main.activity_announcements.* @@ -42,7 +46,7 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, private val viewModel: AnnouncementsViewModel by viewModels { viewModelFactory } - private val adapter = AnnouncementAdapter(emptyList(), this) + private lateinit var adapter: AnnouncementAdapter private val picker by lazy { EmojiPicker(this) } private val pickerDialog by lazy { @@ -75,6 +79,13 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, announcementsList.layoutManager = LinearLayoutManager(this) val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) announcementsList.addItemDecoration(divider) + + val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) + val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) + val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + + adapter = AnnouncementAdapter(emptyList(), this, wellbeingEnabled, animateEmojis) + announcementsList.adapter = adapter viewModel.announcements.observe(this) { @@ -112,16 +123,6 @@ class AnnouncementsActivity : BottomSheetActivity(), AnnouncementActionListener, progressBar.show() } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - private fun refreshAnnouncements() { viewModel.load() swipeRefreshLayout.isRefreshing = true diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index db081d0c..269a70c6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -30,7 +30,6 @@ import android.os.Build import android.os.Bundle import android.os.Parcelable import android.provider.MediaStore -import android.text.TextUtils import android.util.Log import android.view.KeyEvent import android.view.MenuItem @@ -57,19 +56,20 @@ import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.DraftAttachment import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.* import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial @@ -81,7 +81,6 @@ import java.io.File import java.io.IOException import java.util.* import javax.inject.Inject -import kotlin.collections.ArrayList import kotlin.math.max import kotlin.math.min @@ -104,12 +103,13 @@ class ComposeActivity : BaseActivity(), // this only exists when a status is trying to be sent, but uploads are still occurring private var finishingUploadDialog: ProgressDialog? = null private var photoUploadUri: Uri? = null + @VisibleForTesting var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT - private var composeOptions: ComposeOptions? = null private val viewModel: ComposeViewModel by viewModels { viewModelFactory } + private val maxUploadMediaNumber = 4 private var mediaCount = 0 public override fun onCreate(savedInstanceState: Bundle?) { @@ -147,59 +147,45 @@ class ComposeActivity : BaseActivity(), /* If the composer is started up as a reply to another post, override the "starting" state * based on what the intent from the reply request passes. */ - if (intent != null) { - this.composeOptions = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA) - viewModel.setup(composeOptions) - setupReplyViews(composeOptions?.replyingStatusAuthor) - val tootText = composeOptions?.tootText - if (!tootText.isNullOrEmpty()) { - composeEditField.setText(tootText) - } + + val composeOptions: ComposeOptions? = intent.getParcelableExtra(COMPOSE_OPTIONS_EXTRA) + + viewModel.setup(composeOptions) + setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent) + val tootText = composeOptions?.tootText + if (!tootText.isNullOrEmpty()) { + composeEditField.setText(tootText) } - if (!TextUtils.isEmpty(composeOptions?.scheduledAt)) { + if (!composeOptions?.scheduledAt.isNullOrEmpty()) { composeScheduleView.setDateTime(composeOptions?.scheduledAt) } - setupComposeField(viewModel.startingText) + setupComposeField(preferences, viewModel.startingText) setupContentWarningField(composeOptions?.contentWarning) setupPollView() applyShareIntent(intent, savedInstanceState) viewModel.setupComplete.value = true } - private fun applyShareIntent(intent: Intent?, savedInstanceState: Bundle?) { - if (intent != null && savedInstanceState == null) { + private fun applyShareIntent(intent: Intent, savedInstanceState: Bundle?) { + if (savedInstanceState == null) { /* Get incoming images being sent through a share action from another app. Only do this * when savedInstanceState is null, otherwise both the images from the intent and the * instance state will be re-queued. */ - val type = intent.type - if (type != null) { + intent.type?.also { type -> if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) { - val uriList = ArrayList() - if (intent.action != null) { - when (intent.action) { - Intent.ACTION_SEND -> { - val uri = intent.getParcelableExtra(Intent.EXTRA_STREAM) - if (uri != null) { - uriList.add(uri) - } - } - Intent.ACTION_SEND_MULTIPLE -> { - val list = intent.getParcelableArrayListExtra( - Intent.EXTRA_STREAM) - if (list != null) { - for (uri in list) { - if (uri != null) { - uriList.add(uri) - } - } - } + when (intent.action) { + Intent.ACTION_SEND -> { + intent.getParcelableExtra(Intent.EXTRA_STREAM)?.let { uri -> + pickMedia(uri) + } + } + Intent.ACTION_SEND_MULTIPLE -> { + intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM)?.forEach { uri -> + pickMedia(uri) } } - } - for (uri in uriList) { - pickMedia(uri) } } else if (type == "text/plain" && intent.action == Intent.ACTION_SEND) { @@ -217,13 +203,16 @@ class ComposeActivity : BaseActivity(), val left = min(start, end) val right = max(start, end) composeEditField.text.replace(left, right, shareBody, 0, shareBody.length) + // move edittext cursor to first when shareBody parsed + composeEditField.text.insert(0, "\n") + composeEditField.setSelection(0) } } } } } - private fun setupReplyViews(replyingStatusAuthor: String?) { + private fun setupReplyViews(replyingStatusAuthor: String?, replyingStatusContent: String?) { if (replyingStatusAuthor != null) { composeReplyView.show() composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor) @@ -247,7 +236,7 @@ class ComposeActivity : BaseActivity(), } } } - composeOptions?.replyingStatusContent?.let { composeReplyContentView.text = it } + replyingStatusContent?.let { composeReplyContentView.text = it } } private fun setupContentWarningField(startingContentWarning: String?) { @@ -257,13 +246,18 @@ class ComposeActivity : BaseActivity(), composeContentWarningField.onTextChanged { _, _, _, _ -> updateVisibleCharactersLeft() } } - private fun setupComposeField(startingText: String?) { + private fun setupComposeField(preferences: SharedPreferences, startingText: String?) { composeEditField.setOnCommitContentListener(this) composeEditField.setOnKeyListener { _, keyCode, event -> this.onKeyDown(keyCode, event) } composeEditField.setAdapter( - ComposeAutoCompleteAdapter(this)) + ComposeAutoCompleteAdapter( + this, + preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + ) + ) composeEditField.setTokenizer(ComposeTokenizer()) composeEditField.setText(startingText) @@ -650,7 +644,6 @@ class ComposeActivity : BaseActivity(), } } - private fun removePoll() { viewModel.poll.value = null pollPreview.hide() @@ -807,6 +800,7 @@ class ComposeActivity : BaseActivity(), val mimeTypes = arrayOf("image/*", "video/*", "audio/*") intent.type = "*/*" intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) startActivityForResult(intent, MEDIA_PICK_RESULT) } @@ -833,7 +827,23 @@ class ComposeActivity : BaseActivity(), override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) { super.onActivityResult(requestCode, resultCode, intent) if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_PICK_RESULT && intent != null) { - pickMedia(intent.data!!) + if (intent.data != null) { + // Single media, upload it and done. + pickMedia(intent.data!!) + } else if (intent.clipData != null) { + val clipData = intent.clipData!! + val count = clipData.itemCount + if (mediaCount + count > maxUploadMediaNumber) { + // check if exist media + upcoming media > 4, then prob error message. + Toast.makeText(this, getString(R.string.error_upload_max_media_reached, maxUploadMediaNumber), Toast.LENGTH_SHORT).show() + } else { + // if not grater then 4, upload all multiple media. + for (i in 0 until count) { + val imageUri = clipData.getItemAt(i).getUri() + pickMedia(imageUri) + } + } + } } else if (resultCode == Activity.RESULT_OK && requestCode == MEDIA_TAKE_PHOTO_RESULT) { pickMedia(photoUploadUri!!) } @@ -1000,8 +1010,9 @@ class ComposeActivity : BaseActivity(), @Parcelize data class ComposeOptions( // Let's keep fields var until all consumers are Kotlin - var scheduledTootUid: String? = null, + var scheduledTootId: String? = null, var savedTootUid: Int? = null, + var draftId: Int? = null, var tootText: String? = null, var mediaUrls: List? = null, var mediaDescriptions: List? = null, @@ -1013,6 +1024,7 @@ class ComposeActivity : BaseActivity(), var replyingStatusAuthor: String? = null, var replyingStatusContent: String? = null, var mediaAttachments: List? = null, + var draftAttachments: List? = null, var scheduledAt: String? = null, var sensitive: Boolean? = null, var poll: NewPoll? = null, @@ -1039,7 +1051,6 @@ class ComposeActivity : BaseActivity(), } } - @JvmStatic fun canHandleMimeType(mimeType: String?): Boolean { return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain") } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java similarity index 97% rename from app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java rename to app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java index ebc292ab..b2fa94c3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ComposeAutoCompleteAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.java @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.adapter; +package com.keylesspalace.tusky.components.compose; import android.content.Context; import android.preference.PreferenceManager; @@ -53,11 +53,15 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter private final ArrayList resultList; private final AutocompletionProvider autocompletionProvider; + private final boolean animateAvatar; + private final boolean animateEmojis; - public ComposeAutoCompleteAdapter(AutocompletionProvider autocompletionProvider) { + public ComposeAutoCompleteAdapter(AutocompletionProvider autocompletionProvider, boolean animateAvatar, boolean animateEmojis) { super(); resultList = new ArrayList<>(); this.autocompletionProvider = autocompletionProvider; + this.animateAvatar = animateAvatar; + this.animateEmojis = animateEmojis; } @Override @@ -147,15 +151,12 @@ public class ComposeAutoCompleteAdapter extends BaseAdapter ); accountViewHolder.username.setText(formattedUsername); CharSequence emojifiedName = CustomEmojiHelper.emojify(account.getName(), - account.getEmojis(), accountViewHolder.displayName); + account.getEmojis(), accountViewHolder.displayName, animateEmojis); accountViewHolder.displayName.setText(emojifiedName); int avatarRadius = accountViewHolder.avatar.getContext().getResources() .getDimensionPixelSize(R.dimen.avatar_radius_42dp); - boolean animateAvatar = PreferenceManager.getDefaultSharedPreferences(accountViewHolder.avatar.getContext()) - .getBoolean("animateGifAvatars", false); - ImageLoadingHelper.loadAvatar( account.getAvatar(), accountViewHolder.avatar, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 2c015899..71293511 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -21,8 +21,8 @@ import androidx.core.net.toUri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.Observer -import com.keylesspalace.tusky.adapter.ComposeAutoCompleteAdapter import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia +import com.keylesspalace.tusky.components.drafts.DraftHelper import com.keylesspalace.tusky.components.search.SearchType import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase @@ -39,18 +39,12 @@ import io.reactivex.rxkotlin.Singles import java.util.* import javax.inject.Inject -/** - * Throw when trying to add an image when video is already present or the other way around - */ -class VideoOrImageException : Exception() - - -class ComposeViewModel -@Inject constructor( +class ComposeViewModel @Inject constructor( private val api: MastodonApi, private val accountManager: AccountManager, private val mediaUploader: MediaUploader, private val serviceClient: ServiceClient, + private val draftHelper: DraftHelper, private val saveTootHelper: SaveTootHelper, private val db: AppDatabase ) : RxAwareViewModel() { @@ -59,7 +53,8 @@ class ComposeViewModel private var replyingStatusContent: String? = null internal var startingText: String? = null private var savedTootUid: Int = 0 - private var scheduledTootUid: String? = null + private var draftId: Int = 0 + private var scheduledTootId: String? = null private var startingContentWarning: String = "" private var inReplyToId: String? = null private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN @@ -81,10 +76,6 @@ class ComposeViewModel val markMediaAsSensitive = mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false) - fun toggleMarkSensitive() { - this.markMediaAsSensitive.value = !this.markMediaAsSensitive.value!! - } - val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN) val showContentWarning = mutableLiveData(false) val setupComplete = mutableLiveData(false) @@ -96,7 +87,7 @@ class ComposeViewModel private val mediaToDisposable = mutableMapOf() - private val isEditingScheduledToot get() = !scheduledTootUid.isNullOrEmpty() + private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty() init { @@ -116,7 +107,7 @@ class ComposeViewModel .onErrorResumeNext( db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) ) - .subscribe ({ instanceEntity -> + .subscribe({ instanceEntity -> emoji.postValue(instanceEntity.emojiList) instance.postValue(instanceEntity) }, { throwable -> @@ -126,7 +117,7 @@ class ComposeViewModel .autoDispose() } - fun pickMedia(uri: Uri): LiveData> { + fun pickMedia(uri: Uri, description: String? = null): LiveData> { // We are not calling .toLiveData() here because we don't want to stop the process when // the Activity goes away temporarily (like on screen rotation). val liveData = MutableLiveData>() @@ -138,7 +129,7 @@ class ComposeViewModel && mediaItems[0].type == QueuedMedia.Type.IMAGE) { throw VideoOrImageException() } else { - addMediaToQueue(type, uri, size) + addMediaToQueue(type, uri, size, description) } } .subscribe({ queuedMedia -> @@ -150,12 +141,23 @@ class ComposeViewModel return liveData } - private fun addMediaToQueue(type: QueuedMedia.Type, uri: Uri, mediaSize: Long): QueuedMedia { - val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, mediaSize) + private fun addMediaToQueue( + type: QueuedMedia.Type, + uri: Uri, + mediaSize: Long, + description: String? = null + ): QueuedMedia { + val mediaItem = QueuedMedia( + localId = System.currentTimeMillis(), + uri = uri, + type = type, + mediaSize = mediaSize, + description = description + ) media.value = media.value!! + mediaItem mediaToDisposable[mediaItem.localId] = mediaUploader .uploadMedia(mediaItem) - .subscribe ({ event -> + .subscribe({ event -> val item = media.value?.find { it.localId == mediaItem.localId } ?: return@subscribe val newMediaItem = when (event) { @@ -190,6 +192,10 @@ class ComposeViewModel media.value = media.value!!.withoutFirstWhich { it.localId == item.localId } } + fun toggleMarkSensitive() { + this.markMediaAsSensitive.value = this.markMediaAsSensitive.value != true + } + fun didChange(content: String?, contentWarning: String?): Boolean { val textChanged = !(content.isNullOrEmpty() @@ -210,29 +216,37 @@ class ComposeViewModel } fun deleteDraft() { - saveTootHelper.deleteDraft(this.savedTootUid) + if (savedTootUid != 0) { + saveTootHelper.deleteDraft(savedTootUid) + } + if (draftId != 0) { + draftHelper.deleteDraftAndAttachments(draftId) + .subscribe() + } } fun saveDraft(content: String, contentWarning: String) { - val mediaUris = mutableListOf() - val mediaDescriptions = mutableListOf() - for (item in media.value!!) { + + val mediaUris: MutableList = mutableListOf() + val mediaDescriptions: MutableList = mutableListOf() + media.value?.forEach { item -> mediaUris.add(item.uri.toString()) mediaDescriptions.add(item.description) } - saveTootHelper.saveToot( - content, - contentWarning, - null, - mediaUris, - mediaDescriptions, - savedTootUid, - inReplyToId, - replyingStatusContent, - replyingStatusAuthor, - statusVisibility.value!!, - poll.value - ) + + draftHelper.saveDraft( + draftId = draftId, + accountId = accountManager.activeAccount?.id!!, + inReplyToId = inReplyToId, + content = content, + contentWarning = contentWarning, + sensitive = markMediaAsSensitive.value!!, + visibility = statusVisibility.value!!, + mediaUris = mediaUris, + mediaDescriptions = mediaDescriptions, + poll = poll.value, + failedToSend = false + ).subscribe() } /** @@ -246,7 +260,7 @@ class ComposeViewModel ): LiveData { val deletionObservable = if (isEditingScheduledToot) { - api.deleteScheduledStatus(scheduledTootUid.toString()).toObservable().map { Unit } + api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { } } else { just(Unit) }.toLiveData() @@ -264,21 +278,21 @@ class ComposeViewModel } val tootToSend = TootToSend( - content, - spoilerText, - statusVisibility.value!!.serverString(), - mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!), - mediaIds, - mediaUris.map { it.toString() }, - mediaDescriptions, + text = content, + warningText = spoilerText, + visibility = statusVisibility.value!!.serverString(), + sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!), + mediaIds = mediaIds, + mediaUris = mediaUris.map { it.toString() }, + mediaDescriptions = mediaDescriptions, scheduledAt = scheduledAt.value, inReplyToId = inReplyToId, poll = poll.value, replyingStatusContent = null, replyingStatusAuthorUsername = null, - savedJsonUrls = null, accountId = accountManager.activeAccount!!.id, - savedTootUid = 0, + savedTootUid = savedTootUid, + draftId = draftId, idempotencyKey = randomAlphanumericString(16), retries = 0 ) @@ -286,9 +300,7 @@ class ComposeViewModel serviceClient.sendToot(tootToSend) } - return combineLiveData(deletionObservable, sendObservable) { _, _ -> Unit } - - + return combineLiveData(deletionObservable, sendObservable) { _, _ -> } } fun updateDescription(localId: Long, description: String): LiveData { @@ -319,7 +331,6 @@ class ComposeViewModel return completedCaptioningLiveData } - fun searchAutocompleteSuggestions(token: String): List { when (token[0]) { '@' -> { @@ -370,14 +381,12 @@ class ComposeViewModel } } - override fun onCleared() { - for (uploadDisposable in mediaToDisposable.values) { - uploadDisposable.dispose() - } - super.onCleared() - } - fun setup(composeOptions: ComposeActivity.ComposeOptions?) { + + if (setupComplete.value == true) { + return + } + val preferredVisibility = accountManager.activeAccount!!.defaultPostPrivacy val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN @@ -385,6 +394,7 @@ class ComposeViewModel preferredVisibility.num.coerceAtLeast(replyVisibility.num)) inReplyToId = composeOptions?.inReplyToId + modifiedInitialState = composeOptions?.modifiedInitialState == true val contentWarning = composeOptions?.contentWarning @@ -396,10 +406,11 @@ class ComposeViewModel } // recreate media list - // when coming from SavedTootActivity val loadedDraftMediaUris = composeOptions?.mediaUrls val loadedDraftMediaDescriptions: List? = composeOptions?.mediaDescriptions + val draftAttachments = composeOptions?.draftAttachments if (loadedDraftMediaUris != null && loadedDraftMediaDescriptions != null) { + // when coming from SavedTootActivity loadedDraftMediaUris.zip(loadedDraftMediaDescriptions) .forEach { (uri, description) -> pickMedia(uri.toUri()).observeForever { errorOrItem -> @@ -408,23 +419,24 @@ class ComposeViewModel } } } + } else if (draftAttachments != null) { + // when coming from DraftActivity + draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) } } else composeOptions?.mediaAttachments?.forEach { a -> - // when coming from redraft + // when coming from redraft or ScheduledTootActivity val mediaType = when (a.type) { Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO - else -> QueuedMedia.Type.IMAGE } addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description) } - savedTootUid = composeOptions?.savedTootUid ?: 0 - scheduledTootUid = composeOptions?.scheduledTootUid + draftId = composeOptions?.draftId ?: 0 + scheduledTootId = composeOptions?.scheduledTootId startingText = composeOptions?.tootText - val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN if (tootVisibility.num != Status.Visibility.UNKNOWN.num) { startingVisibility = tootVisibility @@ -441,7 +453,6 @@ class ComposeViewModel startingText = builder.toString() } - scheduledAt.value = composeOptions?.scheduledAt composeOptions?.sensitive?.let { markMediaAsSensitive.value = it } @@ -462,6 +473,13 @@ class ComposeViewModel scheduledAt.value = newScheduledAt } + override fun onCleared() { + for (uploadDisposable in mediaToDisposable.values) { + uploadDisposable.dispose() + } + super.onCleared() + } + private companion object { const val TAG = "ComposeViewModel" } @@ -479,4 +497,9 @@ data class ComposeInstanceParams( val pollMaxOptions: Int, val pollMaxLength: Int, val supportsScheduled: Boolean -) \ No newline at end of file +) + +/** + * Thrown when trying to add an image when video is already present or the other way around + */ +class VideoOrImageException : Exception() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt index 1fa03d54..8ff7dcf3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt @@ -173,7 +173,13 @@ class MediaUploaderImpl( val body = MultipartBody.Part.createFormData("file", filename, fileBody) - val uploadDisposable = mastodonApi.uploadMedia(body) + val description = if (media.description != null) { + MultipartBody.Part.createFormData("description", media.description) + } else { + null + } + + val uploadDisposable = mastodonApi.uploadMedia(body, description) .subscribe({ attachment -> emitter.onNext(UploadEvent.FinishedEvent(attachment)) emitter.onComplete() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java index 6727156d..e74be628 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java @@ -75,7 +75,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder { setupCollapsedState(status.getCollapsible(), status.getCollapsed(), status.getExpanded(), status.getSpoilerText(), listener); - setDisplayName(account.getDisplayName(), account.getEmojis()); + setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions); setUsername(account.getUsername()); setCreatedAt(status.getCreatedAt(), statusDisplayOptions); setIsReply(status.getInReplyToId() != null); @@ -83,7 +83,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder { setBookmarked(status.getBookmarked()); List attachments = status.getAttachments(); boolean sensitive = status.getSensitive(); - if (statusDisplayOptions.mediaPreviewEnabled() && !hasAudioAttachment(attachments)) { + if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(), statusDisplayOptions.useBlurhash()); diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index c7dab402..621adae0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -28,12 +28,12 @@ import androidx.recyclerview.widget.SimpleItemAnimator import com.keylesspalace.tusky.AccountActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewTagActivity -import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.fragment.SFragment import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.StatusDisplayOptions @@ -45,8 +45,6 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res @Inject lateinit var viewModelFactory: ViewModelFactory - @Inject - lateinit var db: AppDatabase private val viewModel: ConversationsViewModel by viewModels { viewModelFactory } @@ -68,7 +66,9 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res showBotOverlay = preferences.getBoolean("showBotOverlay", true), useBlurhash = preferences.getBoolean("useBlurhash", true), cardViewMode = CardViewMode.NONE, - confirmReblogs = preferences.getBoolean("confirmReblogs", true) + confirmReblogs = preferences.getBoolean("confirmReblogs", true), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) ) adapter = ConversationAdapter(statusDisplayOptions, this, ::onTopLoaded, viewModel::retry) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt new file mode 100644 index 00000000..5038ac00 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt @@ -0,0 +1,175 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.drafts + +import android.content.Context +import android.net.Uri +import android.util.Log +import android.webkit.MimeTypeMap +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.DraftAttachment +import com.keylesspalace.tusky.db.DraftEntity +import com.keylesspalace.tusky.entity.NewPoll +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.util.IOUtils +import io.reactivex.Completable +import io.reactivex.Observable +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import java.io.File +import java.text.SimpleDateFormat +import java.util.* +import javax.inject.Inject + +class DraftHelper @Inject constructor( + val context: Context, + db: AppDatabase +) { + + private val draftDao = db.draftDao() + + fun saveDraft( + draftId: Int, + accountId: Long, + inReplyToId: String?, + content: String?, + contentWarning: String?, + sensitive: Boolean, + visibility: Status.Visibility, + mediaUris: List, + mediaDescriptions: List, + poll: NewPoll?, + failedToSend: Boolean + ): Completable { + return Single.fromCallable { + + val externalFilesDir = context.getExternalFilesDir("Tusky") + + if (externalFilesDir == null || !(externalFilesDir.exists())) { + Log.e("DraftHelper", "Error obtaining directory to save media.") + throw Exception() + } + + val draftDirectory = File(externalFilesDir, "Drafts") + + if (!draftDirectory.exists()) { + draftDirectory.mkdir() + } + + val uris = mediaUris.map { uriString -> + uriString.toUri() + }.map { uri -> + if (uri.isNotInFolder(draftDirectory)) { + uri.copyToFolder(draftDirectory) + } else { + uri + } + } + + val types = uris.map { uri -> + val mimeType = context.contentResolver.getType(uri) + when (mimeType?.substring(0, mimeType.indexOf('/'))) { + "video" -> DraftAttachment.Type.VIDEO + "image" -> DraftAttachment.Type.IMAGE + "audio" -> DraftAttachment.Type.AUDIO + else -> throw IllegalStateException("unknown media type") + } + } + + val attachments: MutableList = mutableListOf() + for (i in mediaUris.indices) { + attachments.add( + DraftAttachment( + uriString = uris[i].toString(), + description = mediaDescriptions[i], + type = types[i] + ) + ) + } + + DraftEntity( + id = draftId, + accountId = accountId, + inReplyToId = inReplyToId, + content = content, + contentWarning = contentWarning, + sensitive = sensitive, + visibility = visibility, + attachments = attachments, + poll = poll, + failedToSend = failedToSend + ) + + }.flatMapCompletable { draft -> + draftDao.insertOrReplace(draft) + }.subscribeOn(Schedulers.io()) + } + + fun deleteDraftAndAttachments(draftId: Int): Completable { + return draftDao.find(draftId) + .flatMapCompletable { draft -> + deleteDraftAndAttachments(draft) + } + } + + fun deleteDraftAndAttachments(draft: DraftEntity): Completable { + return deleteAttachments(draft) + .andThen(draftDao.delete(draft.id)) + } + + fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) { + draftDao.loadDraftsSingle(accountId) + .flatMapObservable { Observable.fromIterable(it) } + .flatMapCompletable { draft -> + deleteDraftAndAttachments(draft) + }.subscribeOn(Schedulers.io()) + .subscribe() + } + + fun deleteAttachments(draft: DraftEntity): Completable { + return Completable.fromCallable { + draft.attachments.forEach { attachment -> + if (context.contentResolver.delete(attachment.uri, null, null) == 0) { + Log.e("DraftHelper", "Did not delete file ${attachment.uriString}") + } + } + }.subscribeOn(Schedulers.io()) + } + + private fun Uri.isNotInFolder(folder: File): Boolean { + val filePath = path ?: return true + return File(filePath).parentFile == folder + } + + private fun Uri.copyToFolder(folder: File): Uri { + val contentResolver = context.contentResolver + + val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) + + val mimeType = contentResolver.getType(this) + val map = MimeTypeMap.getSingleton() + val fileExtension = map.getExtensionFromMimeType(mimeType) + + val filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension) + val file = File(folder, filename) + IOUtils.copyToFile(contentResolver, this, file) + return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt new file mode 100644 index 00000000..69403fdb --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt @@ -0,0 +1,81 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.drafts + +import android.view.ViewGroup +import android.widget.ImageView +import androidx.appcompat.widget.AppCompatImageView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.db.DraftAttachment + +class DraftMediaAdapter( + private val attachmentClick: () -> Unit +) : ListAdapter( + object: DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean { + return oldItem == newItem + } + + } +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DraftMediaViewHolder { + return DraftMediaViewHolder(AppCompatImageView(parent.context)) + } + + override fun onBindViewHolder(holder: DraftMediaViewHolder, position: Int) { + getItem(position)?.let { attachment -> + if (attachment.type == DraftAttachment.Type.AUDIO) { + holder.imageView.setImageResource(R.drawable.ic_music_box_preview_24dp) + } else { + Glide.with(holder.itemView.context) + .load(attachment.uri) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .dontAnimate() + .into(holder.imageView) + } + } + } + + inner class DraftMediaViewHolder(val imageView: ImageView) + : RecyclerView.ViewHolder(imageView) { + init { + val thumbnailViewSize = + imageView.context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size) + val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize) + val margin = itemView.context.resources + .getDimensionPixelSize(R.dimen.compose_media_preview_margin) + val marginBottom = itemView.context.resources + .getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom) + layoutParams.setMargins(margin, 0, margin, marginBottom) + imageView.layoutParams = layoutParams + imageView.scaleType = ImageView.ScaleType.CENTER_CROP + imageView.setOnClickListener { + attachmentClick() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt new file mode 100644 index 00000000..ddf8a838 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt @@ -0,0 +1,197 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.drafts + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.widget.LinearLayout +import android.widget.Toast +import androidx.activity.viewModels +import androidx.lifecycle.Lifecycle +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.SavedTootActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.databinding.ActivityDraftsBinding +import com.keylesspalace.tusky.db.DraftEntity +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.uber.autodispose.android.lifecycle.autoDispose +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers +import retrofit2.HttpException +import javax.inject.Inject + +class DraftsActivity : BaseActivity(), DraftActionListener { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel: DraftsViewModel by viewModels { viewModelFactory } + + private lateinit var binding: ActivityDraftsBinding + private lateinit var bottomSheet: BottomSheetBehavior + + private var oldDraftsButton: MenuItem? = null + + override fun onCreate(savedInstanceState: Bundle?) { + + super.onCreate(savedInstanceState) + + binding = ActivityDraftsBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.apply { + title = getString(R.string.title_drafts) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + binding.draftsErrorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_saved_status) + + val adapter = DraftsAdapter(this) + + binding.draftsRecyclerView.adapter = adapter + binding.draftsRecyclerView.layoutManager = LinearLayoutManager(this) + binding.draftsRecyclerView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL)) + + bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root) + + viewModel.drafts.observe(this) { draftList -> + if (draftList.isEmpty()) { + binding.draftsRecyclerView.hide() + binding.draftsErrorMessageView.show() + } else { + binding.draftsRecyclerView.show() + binding.draftsErrorMessageView.hide() + adapter.submitList(draftList) + } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.drafts, menu) + oldDraftsButton = menu.findItem(R.id.action_old_drafts) + viewModel.showOldDraftsButton() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { showOldDraftsButton -> + oldDraftsButton?.isVisible = showOldDraftsButton + } + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + android.R.id.home -> { + onBackPressed() + return true + } + R.id.action_old_drafts -> { + val intent = Intent(this, SavedTootActivity::class.java) + startActivityWithSlideInAnimation(intent) + return true + } + } + return super.onOptionsItemSelected(item) + } + + override fun onOpenDraft(draft: DraftEntity) { + + if (draft.inReplyToId != null) { + bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED + viewModel.getToot(draft.inReplyToId) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this) + .subscribe({ status -> + val composeOptions = ComposeActivity.ComposeOptions( + draftId = draft.id, + tootText = draft.content, + contentWarning = draft.contentWarning, + inReplyToId = draft.inReplyToId, + replyingStatusContent = status.content.toString(), + replyingStatusAuthor = status.account.localUsername, + draftAttachments = draft.attachments, + poll = draft.poll, + sensitive = draft.sensitive, + visibility = draft.visibility + ) + + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + + startActivity(ComposeActivity.startIntent(this, composeOptions)) + + }, { throwable -> + + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + + Log.w(TAG, "failed loading reply information", throwable) + + if (throwable is HttpException && throwable.code() == 404) { + // the original status to which a reply was drafted has been deleted + // let's open the ComposeActivity without reply information + Toast.makeText(this, getString(R.string.drafts_toot_reply_removed), Toast.LENGTH_LONG).show() + openDraftWithoutReply(draft) + } else { + Snackbar.make(binding.root, getString(R.string.drafts_failed_loading_reply), Snackbar.LENGTH_SHORT) + .show() + } + }) + } else { + openDraftWithoutReply(draft) + } + } + + private fun openDraftWithoutReply(draft: DraftEntity) { + val composeOptions = ComposeActivity.ComposeOptions( + draftId = draft.id, + tootText = draft.content, + contentWarning = draft.contentWarning, + draftAttachments = draft.attachments, + poll = draft.poll, + sensitive = draft.sensitive, + visibility = draft.visibility + ) + + startActivity(ComposeActivity.startIntent(this, composeOptions)) + } + + override fun onDeleteDraft(draft: DraftEntity) { + viewModel.deleteDraft(draft) + Snackbar.make(binding.root, getString(R.string.draft_deleted), Snackbar.LENGTH_LONG) + .setAction(R.string.action_undo) { + viewModel.restoreDraft(draft) + } + .show() + } + + companion object { + const val TAG = "DraftsActivity" + + fun newIntent(context: Context) = Intent(context, DraftsActivity::class.java) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt new file mode 100644 index 00000000..5dfbceac --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt @@ -0,0 +1,92 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.drafts + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.databinding.ItemDraftBinding +import com.keylesspalace.tusky.db.DraftEntity +import com.keylesspalace.tusky.util.BindingViewHolder +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.visible + +interface DraftActionListener { + fun onOpenDraft(draft: DraftEntity) + fun onDeleteDraft(draft: DraftEntity) +} + +class DraftsAdapter( + private val listener: DraftActionListener +) : PagedListAdapter>( + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { + return oldItem == newItem + } + } +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder { + + val binding = ItemDraftBinding.inflate(LayoutInflater.from(parent.context), parent, false) + + val viewHolder = BindingViewHolder(binding) + + binding.draftMediaPreview.layoutManager = LinearLayoutManager(binding.root.context, RecyclerView.HORIZONTAL, false) + binding.draftMediaPreview.adapter = DraftMediaAdapter { + getItem(viewHolder.adapterPosition)?.let { draft -> + listener.onOpenDraft(draft) + } + } + + return viewHolder + } + + override fun onBindViewHolder(holder: BindingViewHolder, position: Int) { + getItem(position)?.let { draft -> + holder.binding.root.setOnClickListener { + listener.onOpenDraft(draft) + } + holder.binding.deleteButton.setOnClickListener { + listener.onDeleteDraft(draft) + } + holder.binding.draftSendingInfo.visible(draft.failedToSend) + + holder.binding.contentWarning.visible(!draft.contentWarning.isNullOrEmpty()) + holder.binding.contentWarning.text = draft.contentWarning + holder.binding.content.text = draft.content + + holder.binding.draftMediaPreview.visible(draft.attachments.isNotEmpty()) + (holder.binding.draftMediaPreview.adapter as DraftMediaAdapter).submitList(draft.attachments) + + if (draft.poll != null) { + holder.binding.draftPoll.show() + holder.binding.draftPoll.setPoll(draft.poll) + } else { + holder.binding.draftPoll.hide() + } + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt new file mode 100644 index 00000000..f928b6d0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt @@ -0,0 +1,69 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.drafts + +import androidx.lifecycle.ViewModel +import androidx.paging.toLiveData +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.DraftEntity +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import io.reactivex.Observable +import io.reactivex.Single +import javax.inject.Inject + +class DraftsViewModel @Inject constructor( + val database: AppDatabase, + val accountManager: AccountManager, + val api: MastodonApi, + val draftHelper: DraftHelper +) : ViewModel() { + + val drafts = database.draftDao().loadDrafts(accountManager.activeAccount?.id!!).toLiveData(pageSize = 20) + + private val deletedDrafts: MutableList = mutableListOf() + + fun showOldDraftsButton(): Observable { + return database.tootDao().savedTootCount() + .map { count -> count > 0 } + } + + fun deleteDraft(draft: DraftEntity) { + // this does not immediately delete media files to avoid unnecessary file operations + // in case the user decides to restore the draft + database.draftDao().delete(draft.id) + .subscribe() + deletedDrafts.add(draft) + } + + fun restoreDraft(draft: DraftEntity) { + database.draftDao().insertOrReplace(draft) + .subscribe() + deletedDrafts.remove(draft) + } + + fun getToot(tootId: String): Single { + return api.status(tootId) + } + + override fun onCleared() { + deletedDrafts.forEach { + draftHelper.deleteAttachments(it).subscribe() + } + } + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt index f4505ad6..ca04f9c7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/InstanceListActivity.kt @@ -1,7 +1,6 @@ package com.keylesspalace.tusky.components.instancemute import android.os.Bundle -import android.view.MenuItem import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment @@ -32,16 +31,6 @@ class InstanceListActivity: BaseActivity(), HasAndroidInjector { .commit() } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - override fun androidInjector() = androidInjector } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt index bb850bdc..093fc42d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instancemute/fragment/InstanceListFragment.kt @@ -5,6 +5,7 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -14,7 +15,6 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.instancemute.adapter.DomainMutesAdapter import com.keylesspalace.tusky.components.instancemute.interfaces.InstanceActionListener import com.keylesspalace.tusky.di.Injectable -import com.keylesspalace.tusky.fragment.BaseFragment import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.hide @@ -30,7 +30,7 @@ import retrofit2.Response import java.io.IOException import javax.inject.Inject -class InstanceListFragment: BaseFragment(), Injectable, InstanceActionListener { +class InstanceListFragment: Fragment(R.layout.fragment_instance_list), Injectable, InstanceActionListener { @Inject lateinit var api: MastodonApi @@ -39,10 +39,6 @@ class InstanceListFragment: BaseFragment(), Injectable, InstanceActionListener { private var adapter = DomainMutesAdapter(this) private lateinit var scrollListener: EndlessOnScrollListener - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return inflater.inflate(R.layout.fragment_instance_list, container, false) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java index 03c11e29..0622fd47 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationHelper.java @@ -121,7 +121,7 @@ public class NotificationHelper { public static final String CHANNEL_BOOST = "CHANNEL_BOOST"; public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE"; public static final String CHANNEL_POLL = "CHANNEL_POLL"; - + public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS"; /** * WorkManager Tag @@ -138,6 +138,7 @@ public class NotificationHelper { */ public static void make(final Context context, Notification body, AccountEntity account, boolean isFirstOfBatch) { + body = body.rewriteToStatusTypeIfNeeded(account.getAccountId()); if (!filterNotification(account, body, context)) { return; @@ -355,6 +356,7 @@ public class NotificationHelper { CHANNEL_BOOST + account.getIdentifier(), CHANNEL_FAVOURITE + account.getIdentifier(), CHANNEL_POLL + account.getIdentifier(), + CHANNEL_SUBSCRIPTIONS + account.getIdentifier(), }; int[] channelNames = { R.string.notification_mention_name, @@ -362,7 +364,8 @@ public class NotificationHelper { R.string.notification_follow_request_name, R.string.notification_boost_name, R.string.notification_favourite_name, - R.string.notification_poll_name + R.string.notification_poll_name, + R.string.notification_subscription_name, }; int[] channelDescriptions = { R.string.notification_mention_descriptions, @@ -370,7 +373,8 @@ public class NotificationHelper { R.string.notification_follow_request_description, R.string.notification_boost_description, R.string.notification_favourite_description, - R.string.notification_poll_description + R.string.notification_poll_description, + R.string.notification_subscription_description, }; List channels = new ArrayList<>(6); @@ -516,6 +520,8 @@ public class NotificationHelper { switch (notification.getType()) { case MENTION: return account.getNotificationsMentioned(); + case STATUS: + return account.getNotificationsSubscriptions(); case FOLLOW: return account.getNotificationsFollowed(); case FOLLOW_REQUEST: @@ -536,6 +542,8 @@ public class NotificationHelper { switch (notification.getType()) { case MENTION: return CHANNEL_MENTION + account.getIdentifier(); + case STATUS: + return CHANNEL_SUBSCRIPTIONS + account.getIdentifier(); case FOLLOW: return CHANNEL_FOLLOW + account.getIdentifier(); case FOLLOW_REQUEST: @@ -606,6 +614,9 @@ public class NotificationHelper { case MENTION: return String.format(context.getString(R.string.notification_mention_format), accountName); + case STATUS: + return String.format(context.getString(R.string.notification_subscription_format), + accountName); case FOLLOW: return String.format(context.getString(R.string.notification_follow_format), accountName); @@ -636,6 +647,7 @@ public class NotificationHelper { case MENTION: case FAVOURITE: case REBLOG: + case STATUS: if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) { return notification.getStatus().getSpoilerText(); } else { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt index f917d2bb..1e90abc8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt @@ -111,6 +111,17 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable { true } } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_subscriptions) + key = PrefKeys.NOTIFICATION_FILTER_SUBSCRIPTIONS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsSubscriptions + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsSubscriptions = newValue as Boolean } + true + } + } } preferenceCategory(R.string.pref_title_notification_alerts) { category -> diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt index 4fe0abd8..7bb8766a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt @@ -20,8 +20,8 @@ import android.content.Intent import android.content.SharedPreferences import android.os.Bundle import android.util.Log -import android.view.MenuItem import androidx.fragment.app.Fragment +import androidx.fragment.app.commit import androidx.preference.PreferenceManager import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.MainActivity @@ -59,33 +59,36 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference setDisplayShowHomeEnabled(true) } - val fragment: Fragment = when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) { - GENERAL_PREFERENCES -> { - setTitle(R.string.action_view_preferences) - PreferencesFragment.newInstance() - } - ACCOUNT_PREFERENCES -> { - setTitle(R.string.action_view_account_preferences) - AccountPreferencesFragment.newInstance() - } - NOTIFICATION_PREFERENCES -> { - setTitle(R.string.pref_title_edit_notification_settings) - NotificationPreferencesFragment.newInstance() - } - TAB_FILTER_PREFERENCES -> { - setTitle(R.string.pref_title_status_tabs) - TabFilterPreferencesFragment.newInstance() - } - PROXY_PREFERENCES -> { - setTitle(R.string.pref_title_http_proxy_settings) - ProxyPreferencesFragment.newInstance() - } - else -> throw IllegalArgumentException("preferenceType not known") - } + val fragmentTag = "preference_fragment_$EXTRA_PREFERENCE_TYPE" - supportFragmentManager.beginTransaction() - .replace(R.id.fragment_container, fragment) - .commit() + val fragment: Fragment = supportFragmentManager.findFragmentByTag(fragmentTag) + ?: when (intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)) { + GENERAL_PREFERENCES -> { + setTitle(R.string.action_view_preferences) + PreferencesFragment.newInstance() + } + ACCOUNT_PREFERENCES -> { + setTitle(R.string.action_view_account_preferences) + AccountPreferencesFragment.newInstance() + } + NOTIFICATION_PREFERENCES -> { + setTitle(R.string.pref_title_edit_notification_settings) + NotificationPreferencesFragment.newInstance() + } + TAB_FILTER_PREFERENCES -> { + setTitle(R.string.pref_title_status_tabs) + TabFilterPreferencesFragment.newInstance() + } + PROXY_PREFERENCES -> { + setTitle(R.string.pref_title_http_proxy_settings) + ProxyPreferencesFragment.newInstance() + } + else -> throw IllegalArgumentException("preferenceType not known") + } + + supportFragmentManager.commit { + replace(R.id.fragment_container, fragment, fragmentTag) + } restartActivitiesOnExit = intent.getBooleanExtra("restart", false) @@ -101,16 +104,6 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference PreferenceManager.getDefaultSharedPreferences(this).unregisterOnSharedPreferenceChangeListener(this) } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - private fun saveInstanceState(outState: Bundle) { outState.putBoolean("restart", restartActivitiesOnExit) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt index 9214a32a..2c794f70 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt @@ -19,10 +19,14 @@ import android.os.Bundle import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.entity.Notification import com.keylesspalace.tusky.settings.* import com.keylesspalace.tusky.util.ThemeUtils +import com.keylesspalace.tusky.util.deserialize import com.keylesspalace.tusky.util.getNonNullString +import com.keylesspalace.tusky.util.serialize import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt @@ -35,6 +39,9 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable { @Inject lateinit var okhttpclient: OkHttpClient + @Inject + lateinit var accountManager: AccountManager + private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) } private var httpProxyPref: Preference? = null @@ -167,6 +174,13 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable { setTitle(R.string.pref_title_enable_swipe_for_tabs) isSingleLineTitle = false } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.ANIMATE_CUSTOM_EMOJIS + setTitle(R.string.pref_title_animate_custom_emojis) + isSingleLineTitle = false + } } preferenceCategory(R.string.pref_title_browser_settings) { @@ -192,6 +206,45 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable { } } + preferenceCategory(R.string.pref_title_wellbeing_mode) { + switchPreference { + title = getString(R.string.limit_notifications) + setDefaultValue(false) + key = PrefKeys.WELLBEING_LIMITED_NOTIFICATIONS + setOnPreferenceChangeListener { _, value -> + for (account in accountManager.accounts) { + val notificationFilter = deserialize(account.notificationsFilter).toMutableSet() + + if (value == true) { + notificationFilter.add(Notification.Type.FAVOURITE) + notificationFilter.add(Notification.Type.FOLLOW) + notificationFilter.add(Notification.Type.REBLOG) + } else { + notificationFilter.remove(Notification.Type.FAVOURITE) + notificationFilter.remove(Notification.Type.FOLLOW) + notificationFilter.remove(Notification.Type.REBLOG) + } + + account.notificationsFilter = serialize(notificationFilter) + accountManager.saveAccount(account) + } + true + } + } + + switchPreference { + title = getString(R.string.wellbeing_hide_stats_posts) + setDefaultValue(false) + key = PrefKeys.WELLBEING_HIDE_STATS_POSTS + } + + switchPreference { + title = getString(R.string.wellbeing_hide_stats_profile) + setDefaultValue(false) + key = PrefKeys.WELLBEING_HIDE_STATS_PROFILE + } + } + preferenceCategory(R.string.pref_title_proxy_settings) { httpProxyPref = preference { setTitle(R.string.pref_title_http_proxy_settings) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt index 3ecadd58..2c7f2d46 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt @@ -18,7 +18,6 @@ package com.keylesspalace.tusky.components.report import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.MenuItem import androidx.activity.viewModels import com.keylesspalace.tusky.BottomSheetActivity import com.keylesspalace.tusky.R @@ -30,7 +29,6 @@ import kotlinx.android.synthetic.main.activity_report.* import kotlinx.android.synthetic.main.toolbar_basic.* import javax.inject.Inject - class ReportActivity : BottomSheetActivity(), HasAndroidInjector { @Inject @@ -120,16 +118,6 @@ class ReportActivity : BottomSheetActivity(), HasAndroidInjector { wizard.currentItem = 0 } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - closeScreen() - return true - } - } - return super.onOptionsItemSelected(item) - } - companion object { private const val ACCOUNT_ID = "account_id" private const val ACCOUNT_USERNAME = "account_username" diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt index 93b3a7d2..8201de2e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt @@ -75,7 +75,7 @@ class StatusViewHolder( sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive), mediaViewHeight) - statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions.useAbsoluteTime) + statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions) setCreatedAt(status.createdAt) } @@ -89,7 +89,7 @@ class StatusViewHolder( itemView.statusContentWarningButton.hide() itemView.statusContentWarningDescription.hide() } else { - val emojiSpoiler = status.spoilerText.emojify(status.emojis, itemView.statusContentWarningDescription) + val emojiSpoiler = status.spoilerText.emojify(status.emojis, itemView.statusContentWarningDescription, statusDisplayOptions.animateEmojis) itemView.statusContentWarningDescription.text = emojiSpoiler itemView.statusContentWarningDescription.show() itemView.statusContentWarningButton.show() @@ -122,7 +122,7 @@ class StatusViewHolder( emojis: List, listener: LinkListener) { if (expanded) { - val emojifiedText = content.emojify(emojis, itemView.statusContent) + val emojifiedText = content.emojify(emojis, itemView.statusContent, statusDisplayOptions.animateEmojis) LinkHelper.setClickableText(itemView.statusContent, emojifiedText, mentions, listener) } else { LinkHelper.setClickableMentions(itemView.statusContent, mentions, listener) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt index 6151903f..03bd8ef9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt @@ -15,13 +15,10 @@ package com.keylesspalace.tusky.components.report.fragments - import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.report.Screen @@ -33,19 +30,12 @@ import com.keylesspalace.tusky.util.show import kotlinx.android.synthetic.main.fragment_report_done.* import javax.inject.Inject - -class ReportDoneFragment : Fragment(), Injectable { +class ReportDoneFragment : Fragment(R.layout.fragment_report_done), Injectable { @Inject lateinit var viewModelFactory: ViewModelFactory - private val viewModel: ReportViewModel by viewModels({ requireActivity() }) { viewModelFactory } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_report_done, container, false) - } + private val viewModel: ReportViewModel by activityViewModels { viewModelFactory } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt index 4e7b00ab..b933b2fa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt @@ -16,12 +16,10 @@ package com.keylesspalace.tusky.components.report.fragments import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.report.ReportViewModel @@ -33,18 +31,12 @@ import kotlinx.android.synthetic.main.fragment_report_note.* import java.io.IOException import javax.inject.Inject -class ReportNoteFragment : Fragment(), Injectable { +class ReportNoteFragment : Fragment(R.layout.fragment_report_note), Injectable { @Inject lateinit var viewModelFactory: ViewModelFactory - private val viewModel: ReportViewModel by viewModels({ requireActivity() }) { viewModelFactory } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_report_note, container, false) - } + private val viewModel: ReportViewModel by activityViewModels { viewModelFactory } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { fillViews() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt index 2fb4132a..66b49b2b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt @@ -16,13 +16,11 @@ package com.keylesspalace.tusky.components.report.fragments import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.core.app.ActivityOptionsCompat import androidx.core.view.ViewCompat import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -41,6 +39,7 @@ import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.hide @@ -49,7 +48,7 @@ import com.keylesspalace.tusky.viewdata.AttachmentViewData import kotlinx.android.synthetic.main.fragment_report_statuses.* import javax.inject.Inject -class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler { +class ReportStatusesFragment : Fragment(R.layout.fragment_report_statuses), Injectable, AdapterHandler { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -57,10 +56,9 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler { @Inject lateinit var accountManager: AccountManager - private val viewModel: ReportViewModel by viewModels({ requireActivity() }) { viewModelFactory } + private val viewModel: ReportViewModel by activityViewModels { viewModelFactory } private lateinit var adapter: StatusesAdapter - private lateinit var layoutManager: LinearLayoutManager private var snackbarErrorRetry: Snackbar? = null @@ -88,12 +86,6 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_report_statuses, container, false) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { handleClicks() initStatusesView() @@ -118,15 +110,16 @@ class ReportStatusesFragment : Fragment(), Injectable, AdapterHandler { showBotOverlay = false, useBlurhash = preferences.getBoolean("useBlurhash", true), cardViewMode = CardViewMode.NONE, - confirmReblogs = preferences.getBoolean("confirmReblogs", true) + confirmReblogs = preferences.getBoolean("confirmReblogs", true), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) ) adapter = StatusesAdapter(statusDisplayOptions, viewModel.statusViewState, this) recyclerView.addItemDecoration(DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)) - layoutManager = LinearLayoutManager(requireContext()) - recyclerView.layoutManager = layoutManager + recyclerView.layoutManager = LinearLayoutManager(requireContext()) recyclerView.adapter = adapter (recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt index 98fb5f84..66eb0983 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledTootActivity.kt @@ -18,7 +18,6 @@ package com.keylesspalace.tusky.components.scheduled import android.content.Context import android.content.Intent import android.os.Bundle -import android.view.MenuItem import androidx.lifecycle.ViewModelProvider import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager @@ -104,23 +103,13 @@ class ScheduledTootActivity : BaseActivity(), ScheduledTootActionListener, Injec } } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - private fun refreshStatuses() { viewModel.reload() } override fun edit(item: ScheduledStatus) { val intent = ComposeActivity.startIntent(this, ComposeActivity.ComposeOptions( - scheduledTootUid = item.id, + scheduledTootId = item.id, tootText = item.params.text, contentWarning = item.params.spoilerText, mediaAttachments = item.mediaAttachments, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt index 8b8d1ef4..be705637 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt @@ -20,7 +20,6 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.view.Menu -import android.view.MenuItem import androidx.activity.viewModels import androidx.appcompat.widget.SearchView import com.google.android.material.tabs.TabLayoutMediator @@ -82,17 +81,7 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector { return true } - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - android.R.id.home -> { - onBackPressed() - return true - } - } - return super.onOptionsItemSelected(item) - } - - private fun getPageTitle(position: Int): CharSequence? { + private fun getPageTitle(position: Int): CharSequence { return when (position) { 0 -> getString(R.string.title_statuses) 1 -> getString(R.string.title_accounts) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index da2c3fb1..08afe44d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -193,8 +193,8 @@ class SearchViewModel @Inject constructor( return accountManager.getAllAccountsOrderedByActive() } - fun muteAccount(accountId: String, notifications: Boolean) { - timelineCases.mute(accountId, notifications) + fun muteAccount(accountId: String, notifications: Boolean, duration: Int) { + timelineCases.mute(accountId, notifications, duration) } fun pinAccount(status: Status, isPin: Boolean) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt index c135ad7d..b6bc9568 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt @@ -25,7 +25,7 @@ import com.keylesspalace.tusky.adapter.AccountViewHolder import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.interfaces.LinkListener -class SearchAccountsAdapter(private val linkListener: LinkListener) +class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean) : PagedListAdapter(ACCOUNT_COMPARATOR) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { @@ -37,7 +37,7 @@ class SearchAccountsAdapter(private val linkListener: LinkListener) override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { getItem(position)?.let { item -> (holder as AccountViewHolder).apply { - setupWithAccount(item) + setupWithAccount(item, animateAvatars, animateEmojis) setupLinkListener(linkListener) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt index 714580f7..c453f97c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt @@ -18,12 +18,23 @@ package com.keylesspalace.tusky.components.search.fragments import androidx.lifecycle.LiveData import androidx.paging.PagedList import androidx.paging.PagedListAdapter +import androidx.preference.PreferenceManager import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.NetworkState +import kotlinx.android.synthetic.main.fragment_search.* class SearchAccountsFragment : SearchFragment() { - override fun createAdapter(): PagedListAdapter = SearchAccountsAdapter(this) + override fun createAdapter(): PagedListAdapter { + val preferences = PreferenceManager.getDefaultSharedPreferences(searchRecyclerView.context) + + return SearchAccountsAdapter( + this, + preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + ) + } override val networkStateRefresh: LiveData get() = viewModel.networkStateAccountRefresh diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt index 2c06431e..7985d5ba 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt @@ -1,11 +1,9 @@ package com.keylesspalace.tusky.components.search.fragments import android.os.Bundle -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels +import androidx.fragment.app.activityViewModels import androidx.lifecycle.LiveData import androidx.paging.PagedList import androidx.paging.PagedListAdapter @@ -26,13 +24,13 @@ import com.keylesspalace.tusky.util.* import kotlinx.android.synthetic.main.fragment_search.* import javax.inject.Inject -abstract class SearchFragment : Fragment(), +abstract class SearchFragment : Fragment(R.layout.fragment_search), LinkListener, Injectable, SwipeRefreshLayout.OnRefreshListener { @Inject lateinit var viewModelFactory: ViewModelFactory - protected val viewModel: SearchViewModel by viewModels({ requireActivity() }) { viewModelFactory } + protected val viewModel: SearchViewModel by activityViewModels { viewModelFactory } private var snackbarErrorRetry: Snackbar? = null @@ -43,12 +41,7 @@ abstract class SearchFragment : Fragment(), abstract val data: LiveData> protected lateinit var adapter: PagedListAdapter - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_search, container, false) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) initAdapter() setupSwipeRefreshLayout() subscribeObservables() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt index ce5c2612..6d96bb5a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -52,7 +52,9 @@ import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status.Mention import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.NetworkState import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.view.showMuteAccountDialog @@ -84,7 +86,9 @@ class SearchStatusesFragment : SearchFragment { + LinkHelper.openLink(actionable.attachments[attachmentIndex].url, context) } } @@ -375,8 +380,8 @@ class SearchStatusesFragment : SearchFragment - viewModel.muteAccount(accountId, notifications) + ) { notifications, duration -> + viewModel.muteAccount(accountId, notifications, duration) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt index 77ce16cc..ab6dbb7e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt @@ -43,6 +43,7 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long, var notificationsReblogged: Boolean = true, var notificationsFavorited: Boolean = true, var notificationsPolls: Boolean = true, + var notificationsSubscriptions: Boolean = true, var notificationSound: Boolean = true, var notificationVibration: Boolean = true, var notificationLight: Boolean = true, diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt index 138f71a3..fc10adb6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt @@ -35,7 +35,8 @@ class AccountManager @Inject constructor(db: AppDatabase) { @Volatile var activeAccount: AccountEntity? = null - private var accounts: MutableList = mutableListOf() + var accounts: MutableList = mutableListOf() + private set private val accountDao: AccountDao = db.accountDao() init { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 608333d9..d35fd389 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -15,22 +15,22 @@ package com.keylesspalace.tusky.db; -import com.keylesspalace.tusky.TabDataKt; -import com.keylesspalace.tusky.components.conversation.ConversationEntity; - -import androidx.sqlite.db.SupportSQLiteDatabase; +import androidx.annotation.NonNull; import androidx.room.Database; import androidx.room.RoomDatabase; import androidx.room.migration.Migration; -import androidx.annotation.NonNull; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import com.keylesspalace.tusky.TabDataKt; +import com.keylesspalace.tusky.components.conversation.ConversationEntity; /** * DB version & declare DAO */ -@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, +@Database(entities = { TootEntity.class, DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, TimelineAccountEntity.class, ConversationEntity.class - }, version = 23) + }, version = 25) public abstract class AppDatabase extends RoomDatabase { public abstract TootDao tootDao(); @@ -38,6 +38,7 @@ public abstract class AppDatabase extends RoomDatabase { public abstract InstanceDao instanceDao(); public abstract ConversationsDao conversationDao(); public abstract TimelineDao timelineDao(); + public abstract DraftDao draftDao(); public static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override @@ -46,7 +47,6 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("INSERT INTO TootEntity2 SELECT * FROM TootEntity;"); database.execSQL("DROP TABLE TootEntity;"); database.execSQL("ALTER TABLE TootEntity2 RENAME TO TootEntity;"); - } }; @@ -339,5 +339,30 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `muted` INTEGER"); } }; + + public static final Migration MIGRATION_23_24 = new Migration(23, 24) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSubscriptions` INTEGER NOT NULL DEFAULT 1"); + } + }; + public static final Migration MIGRATION_24_25 = new Migration(24, 25) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "CREATE TABLE IF NOT EXISTS `DraftEntity` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`accountId` INTEGER NOT NULL, " + + "`inReplyToId` TEXT," + + "`content` TEXT," + + "`contentWarning` TEXT," + + "`sensitive` INTEGER NOT NULL," + + "`visibility` INTEGER NOT NULL," + + "`attachments` TEXT NOT NULL," + + "`poll` TEXT," + + "`failedToSend` INTEGER NOT NULL)" + ); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt index 3492deda..1b1f94f3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -24,10 +24,7 @@ import com.google.gson.reflect.TypeToken import com.keylesspalace.tusky.TabData import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity import com.keylesspalace.tusky.createTabDataFromId -import com.keylesspalace.tusky.entity.Attachment -import com.keylesspalace.tusky.entity.Emoji -import com.keylesspalace.tusky.entity.Poll -import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.* import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.util.trimTrailingWhitespace import java.net.URLDecoder @@ -151,4 +148,23 @@ class Converters { return gson.fromJson(pollJson, Poll::class.java) } -} \ No newline at end of file + @TypeConverter + fun newPollToJson(newPoll: NewPoll?): String? { + return gson.toJson(newPoll) + } + + @TypeConverter + fun jsonToNewPoll(newPollJson: String?): NewPoll? { + return gson.fromJson(newPollJson, NewPoll::class.java) + } + + @TypeConverter + fun draftAttachmentListToJson(draftAttachments: List?): String? { + return gson.toJson(draftAttachments) + } + + @TypeConverter + fun jsonToDraftAttachmentList(draftAttachmentListJson: String?): List? { + return gson.fromJson(draftAttachmentListJson, object : TypeToken>() {}.type) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt new file mode 100644 index 00000000..065af1ae --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt @@ -0,0 +1,44 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.db + +import androidx.paging.DataSource +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import io.reactivex.Completable +import io.reactivex.Single + +@Dao +interface DraftDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrReplace(draft: DraftEntity): Completable + + @Query("SELECT * FROM DraftEntity WHERE accountId = :accountId ORDER BY id ASC") + fun loadDrafts(accountId: Long): DataSource.Factory + + @Query("SELECT * FROM DraftEntity WHERE accountId = :accountId") + fun loadDraftsSingle(accountId: Long): Single> + + @Query("DELETE FROM DraftEntity WHERE id = :id") + fun delete(id: Int): Completable + + @Query("SELECT * FROM DraftEntity WHERE id = :id") + fun find(id: Int): Single + +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt new file mode 100644 index 00000000..be1eca58 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt @@ -0,0 +1,55 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.db + +import android.net.Uri +import android.os.Parcelable +import androidx.core.net.toUri +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.keylesspalace.tusky.entity.NewPoll +import com.keylesspalace.tusky.entity.Status +import kotlinx.android.parcel.Parcelize + +@Entity +@TypeConverters(Converters::class) +data class DraftEntity( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val accountId: Long, + val inReplyToId: String?, + val content: String?, + val contentWarning: String?, + val sensitive: Boolean, + val visibility: Status.Visibility, + val attachments: List, + val poll: NewPoll?, + val failedToSend: Boolean +) + +@Parcelize +data class DraftAttachment( + val uriString: String, + val description: String?, + val type: Type +): Parcelable { + val uri: Uri + get() = uriString.toUri() + + enum class Type { + IMAGE, VIDEO, AUDIO; + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt index da98cb4b..296111d3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt @@ -26,7 +26,7 @@ import com.keylesspalace.tusky.entity.Status // Avoiding rescanning status table when accounts table changes. Recommended by Room(c). indices = [Index("authorServerId", "timelineUserId")] ) -@TypeConverters(TootEntity.Converters::class) +@TypeConverters(Converters::class) data class TimelineStatusEntity( val serverId: String, // id never flips: we need it for sorting so it's a real id val url: String?, diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java b/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java index c121e170..f46c2753 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/TootDao.java @@ -16,12 +16,12 @@ package com.keylesspalace.tusky.db; import androidx.room.Dao; -import androidx.room.Insert; -import androidx.room.OnConflictStrategy; import androidx.room.Query; import java.util.List; +import io.reactivex.Observable; + /** * Created by cto3543 on 28/06/2017. * @@ -30,8 +30,6 @@ import java.util.List; @Dao public interface TootDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - void insertOrReplace(TootEntity users); @Query("SELECT * FROM TootEntity ORDER BY uid DESC") List loadAll(); @@ -41,4 +39,7 @@ public interface TootDao { @Query("SELECT * FROM TootEntity WHERE uid = :uid") TootEntity find(int uid); -} + + @Query("SELECT COUNT(*) FROM TootEntity") + Observable savedTootCount(); +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt index 0257c28f..2e82d640 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -18,6 +18,7 @@ package com.keylesspalace.tusky.di import com.keylesspalace.tusky.* import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.components.drafts.DraftsActivity import com.keylesspalace.tusky.components.instancemute.InstanceListActivity import com.keylesspalace.tusky.components.preference.PreferencesActivity import com.keylesspalace.tusky.components.report.ReportActivity @@ -107,4 +108,7 @@ abstract class ActivitiesModule { @ContributesAndroidInjector abstract fun contributesAnnouncementsActivity(): AnnouncementsActivity + + @ContributesAndroidInjector + abstract fun contributesDraftActivity(): DraftsActivity } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index aedcc3ae..1137b12b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -80,7 +80,7 @@ class AppModule { AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16, AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22, - AppDatabase.MIGRATION_22_23) + AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25) .build() } @@ -88,4 +88,4 @@ class AppModule { @Singleton fun notifier(context: Context): Notifier = SystemNotifier(context) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index ca60bac8..fb232a64 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -16,6 +16,8 @@ package com.keylesspalace.tusky.di import android.content.Context +import android.content.SharedPreferences +import android.os.Build import android.text.Spanned import com.google.gson.Gson import com.google.gson.GsonBuilder @@ -24,15 +26,20 @@ import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.okhttpClient +import com.keylesspalace.tusky.util.getNonNullString import dagger.Module import dagger.Provides +import okhttp3.Cache +import okhttp3.OkHttp import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor import retrofit2.Retrofit import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory import retrofit2.converter.gson.GsonConverterFactory import retrofit2.create +import java.net.InetSocketAddress +import java.net.Proxy +import java.util.concurrent.TimeUnit import javax.inject.Singleton /** @@ -54,9 +61,37 @@ class NetworkModule { @Singleton fun providesHttpClient( accountManager: AccountManager, - context: Context + context: Context, + preferences: SharedPreferences ): OkHttpClient { - return okhttpClient(context) + val httpProxyEnabled = preferences.getBoolean("httpProxyEnabled", false) + val httpServer = preferences.getNonNullString("httpProxyServer", "") + val httpPort = preferences.getNonNullString("httpProxyPort", "-1").toIntOrNull() ?: -1 + val cacheSize = 25 * 1024 * 1024L // 25 MiB + val builder = OkHttpClient.Builder() + .addInterceptor { chain -> + /** + * Add a custom User-Agent that contains Tusky, Android and OkHttp Version to all requests + * Example: + * User-Agent: Tusky/1.1.2 Android/5.0.2 OkHttp/4.9.0 + * */ + val requestWithUserAgent = chain.request().newBuilder() + .header( + "User-Agent", + "Tusky/${BuildConfig.VERSION_NAME} Android/${Build.VERSION.RELEASE} OkHttp/${OkHttp.VERSION}" + ) + .build() + chain.proceed(requestWithUserAgent) + } + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .cache(Cache(context.cacheDir, cacheSize)) + + if (httpProxyEnabled && httpServer.isNotEmpty() && httpPort > 0 && httpPort < 65535) { + val address = InetSocketAddress.createUnresolved(httpServer, httpPort) + builder.proxy(Proxy(Proxy.Type.HTTP, address)) + } + return builder .apply { addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) if (BuildConfig.DEBUG) { diff --git a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt index c461012d..ce83deda 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.ViewModelProvider import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.conversation.ConversationsViewModel +import com.keylesspalace.tusky.components.drafts.DraftsViewModel import com.keylesspalace.tusky.components.report.ReportViewModel import com.keylesspalace.tusky.components.scheduled.ScheduledTootViewModel import com.keylesspalace.tusky.components.search.SearchViewModel @@ -91,5 +92,10 @@ abstract class ViewModelModule { @ViewModelKey(AnnouncementsViewModel::class) internal abstract fun announcementsViewModel(viewModel: AnnouncementsViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(DraftsViewModel::class) + internal abstract fun draftsViewModel(viewModel: DraftsViewModel): ViewModel + //Add more ViewModels here } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt index fe342e7a..0dbefd61 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt @@ -15,7 +15,10 @@ package com.keylesspalace.tusky.entity -import com.google.gson.* +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import com.google.gson.JsonParseException import com.google.gson.annotations.JsonAdapter data class Notification( @@ -32,7 +35,8 @@ data class Notification( FAVOURITE("favourite"), FOLLOW("follow"), FOLLOW_REQUEST("follow_request"), - POLL("poll"); + POLL("poll"), + STATUS("status"); companion object { @@ -44,7 +48,7 @@ data class Notification( } return UNKNOWN } - val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL) + val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS) } override fun toString(): String { @@ -72,4 +76,14 @@ data class Notification( } } + + // for Pleroma compatibility that uses Mention type + fun rewriteToStatusTypeIfNeeded(accountId: String) : Notification { + if (type == Type.MENTION && status != null) { + return if (status.mentions.any { + it.id == accountId + }) this else copy(type = Type.STATUS) + } + return this + } } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt index e6a75c51..e25a3d10 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt @@ -26,6 +26,8 @@ data class Relationship ( @SerializedName("muting_notifications") val mutingNotifications: Boolean, val requested: Boolean, @SerializedName("showing_reblogs") val showingReblogs: Boolean, + val subscribing: Boolean? = null, // Pleroma extension @SerializedName("domain_blocking") val blockingDomain: Boolean, - val note: String? // nullable for backward compatibility / feature detection + val note: String?, // nullable for backward compatibility / feature detection + val notifying: Boolean? // since 3.3.0rc ) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt index da9893a2..7b202713 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountListFragment.kt @@ -17,10 +17,10 @@ package com.keylesspalace.tusky.fragment import android.os.Bundle import android.util.Log -import android.view.LayoutInflater import android.view.View -import android.view.ViewGroup +import androidx.fragment.app.Fragment import androidx.lifecycle.Lifecycle +import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -36,6 +36,7 @@ import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.interfaces.AccountActionListener import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.settings.PrefKeys import com.keylesspalace.tusky.util.HttpHeaderLink import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show @@ -45,14 +46,12 @@ import com.uber.autodispose.autoDispose import io.reactivex.Single import io.reactivex.android.schedulers.AndroidSchedulers import kotlinx.android.synthetic.main.fragment_account_list.* -import retrofit2.Call -import retrofit2.Callback import retrofit2.Response import java.io.IOException -import java.util.HashMap +import java.util.* import javax.inject.Inject -class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { +class AccountListFragment : Fragment(R.layout.fragment_account_list), AccountActionListener, Injectable { @Inject lateinit var api: MastodonApi @@ -71,10 +70,6 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { id = arguments?.getString(ARG_ID) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - return inflater.inflate(R.layout.fragment_account_list, container, false) - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -85,11 +80,15 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) + val pm = PreferenceManager.getDefaultSharedPreferences(view.context) + val animateAvatar = pm.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) + val animateEmojis = pm.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + adapter = when (type) { - Type.BLOCKS -> BlocksAdapter(this) - Type.MUTES -> MutesAdapter(this) - Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this) - else -> FollowAdapter(this) + Type.BLOCKS -> BlocksAdapter(this, animateAvatar, animateEmojis) + Type.MUTES -> MutesAdapter(this, animateAvatar, animateEmojis) + Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this, animateAvatar, animateEmojis) + else -> FollowAdapter(this, animateAvatar, animateEmojis) } recyclerView.adapter = adapter @@ -202,27 +201,23 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { override fun onRespondToFollowRequest(accept: Boolean, accountId: String, position: Int) { - val callback = object : Callback { - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - onRespondToFollowRequestSuccess(position) - } else { - onRespondToFollowRequestFailure(accept, accountId) - } - } - - override fun onFailure(call: Call, t: Throwable) { - onRespondToFollowRequestFailure(accept, accountId) - } - } - - val call = if (accept) { + if (accept) { api.authorizeFollowRequest(accountId) } else { api.rejectFollowRequest(accountId) - } - callList.add(call) - call.enqueue(callback) + }.observeOn(AndroidSchedulers.mainThread()) + .autoDispose(from(this, Lifecycle.Event.ON_DESTROY)) + .subscribe({ + onRespondToFollowRequestSuccess(position) + }, { throwable -> + val verb = if (accept) { + "accept" + } else { + "reject" + } + Log.e(TAG, "Failed to $verb account id $accountId.", throwable) + }) + } private fun onRespondToFollowRequestSuccess(position: Int) { @@ -230,15 +225,6 @@ class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { followRequestsAdapter.removeItem(position) } - private fun onRespondToFollowRequestFailure(accept: Boolean, accountId: String) { - val verb = if (accept) { - "accept" - } else { - "reject" - } - Log.e(TAG, "Failed to $verb account id $accountId.") - } - private fun getFetchCallByListType(fromId: String?): Single>> { return when (type) { Type.FOLLOWS -> { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt index 1e42aa9b..d64ffdfc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt @@ -18,12 +18,13 @@ package com.keylesspalace.tusky.fragment import android.graphics.Color import android.os.Bundle import android.util.Log -import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import androidx.core.app.ActivityOptionsCompat import androidx.core.view.ViewCompat +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide @@ -34,14 +35,17 @@ import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.interfaces.RefreshableFragment import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.LinkHelper import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.view.SquareImageView import com.keylesspalace.tusky.viewdata.AttachmentViewData +import com.uber.autodispose.android.lifecycle.autoDispose +import io.reactivex.SingleObserver +import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.disposables.Disposable import kotlinx.android.synthetic.main.fragment_timeline.* -import retrofit2.Call -import retrofit2.Callback import retrofit2.Response import java.io.IOException import java.util.* @@ -53,7 +57,7 @@ import javax.inject.Inject * Fragment with multiple columns of media previews for the specified account. */ -class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { +class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFragment, Injectable { companion object { @JvmStatic fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment { @@ -77,14 +81,13 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { lateinit var api: MastodonApi private val adapter = MediaGridAdapter() - private var currentCall: Call>? = null private val statuses = mutableListOf() private var fetchingStatus = FetchingStatus.NOT_FETCHING private lateinit var accountId: String - private val callback = object : Callback> { - override fun onFailure(call: Call>?, t: Throwable?) { + private val callback = object : SingleObserver>> { + override fun onError(t: Throwable) { fetchingStatus = FetchingStatus.NOT_FETCHING if (isAdded) { @@ -106,7 +109,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { Log.d(TAG, "Failed to fetch account media", t) } - override fun onResponse(call: Call>, response: Response>) { + override fun onSuccess(response: Response>) { fetchingStatus = FetchingStatus.NOT_FETCHING if (isAdded) { swipeRefreshLayout.isRefreshing = false @@ -127,22 +130,23 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { if (statuses.isEmpty()) { statusView.show() - statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, - null) + statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty) } } } } + + override fun onSubscribe(d: Disposable) {} } - private val bottomCallback = object : Callback> { - override fun onFailure(call: Call>?, t: Throwable?) { + private val bottomCallback = object : SingleObserver>> { + override fun onError(t: Throwable) { fetchingStatus = FetchingStatus.NOT_FETCHING Log.d(TAG, "Failed to fetch account media", t) } - override fun onResponse(call: Call>, response: Response>) { + override fun onSuccess(response: Response>) { fetchingStatus = FetchingStatus.NOT_FETCHING val body = response.body() body?.let { fetched -> @@ -159,6 +163,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { } } + override fun onSubscribe(d: Disposable) { } } override fun onCreate(savedInstanceState: Bundle?) { @@ -166,10 +171,6 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,true) == true accountId = arguments?.getString(ACCOUNT_ID_ARG)!! } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle?): View? { - return inflater.inflate(R.layout.fragment_timeline, container, false) - } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -201,8 +202,10 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { statuses.lastOrNull()?.let { (id) -> Log.d(TAG, "Requesting statuses with max_id: ${id}, (bottom)") fetchingStatus = FetchingStatus.FETCHING_BOTTOM - currentCall = api.accountStatuses(accountId, id, null, null, null, true, null) - currentCall?.enqueue(bottomCallback) + api.accountStatuses(accountId, id, null, null, null, true, null) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY) + .subscribe(bottomCallback) } } } @@ -215,14 +218,15 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { private fun refresh() { statusView.hide() if (fetchingStatus != FetchingStatus.NOT_FETCHING) return - currentCall = if (statuses.isEmpty()) { + if (statuses.isEmpty()) { fetchingStatus = FetchingStatus.INITIAL_FETCHING api.accountStatuses(accountId, null, null, null, null, true, null) } else { fetchingStatus = FetchingStatus.REFRESHING api.accountStatuses(accountId, null, statuses[0].id, null, null, true, null) - } - currentCall?.enqueue(callback) + }.observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe(callback) if (!isSwipeToRefreshEnabled) topProgressBar?.show() @@ -234,8 +238,10 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { } if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) { fetchingStatus = FetchingStatus.INITIAL_FETCHING - currentCall = api.accountStatuses(accountId, null, null, null, null, true, null) - currentCall?.enqueue(callback) + api.accountStatuses(accountId, null, null, null, null, true, null) + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY) + .subscribe(callback) } else if (needToRefresh) refresh() @@ -260,10 +266,8 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { } } Attachment.Type.UNKNOWN -> { - }/* Intentionally do nothing. This case is here is to handle when new attachment - * types are added to the API before code is added here to handle them. So, the - * best fallback is to just show the preview and ignore requests to view them. */ - + LinkHelper.openLink(items[currentIndex].attachment.url, context) + } } } @@ -340,5 +344,4 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable { needToRefresh = true } - } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java deleted file mode 100644 index b674b8ba..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/BaseFragment.java +++ /dev/null @@ -1,43 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.fragment; - -import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; - -import java.util.ArrayList; -import java.util.List; - -import retrofit2.Call; - -public class BaseFragment extends Fragment { - protected List callList; - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - callList = new ArrayList<>(); - } - - @Override - public void onDestroy() { - for (Call call : callList) { - call.cancel(); - } - super.onDestroy(); - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 4e4054f2..6ee66f42 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -71,6 +71,7 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener; import com.keylesspalace.tusky.interfaces.ActionButtonActivity; import com.keylesspalace.tusky.interfaces.ReselectableFragment; import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.settings.PrefKeys; import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.Either; import com.keylesspalace.tusky.util.HttpHeaderLink; @@ -101,13 +102,11 @@ import at.connyduck.sparkbutton.helpers.Utils; import io.reactivex.Observable; import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; import kotlin.Unit; import kotlin.collections.CollectionsKt; import kotlin.jvm.functions.Function1; -import okhttp3.ResponseBody; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; import static com.keylesspalace.tusky.util.StringUtils.isLessThan; import static com.uber.autodispose.AutoDispose.autoDisposable; @@ -124,8 +123,9 @@ public class NotificationsFragment extends SFragment implements private static final int LOAD_AT_ONCE = 30; private int maxPlaceholderId = 0; + private final Set notificationFilter = new HashSet<>(); - private Set notificationFilter = new HashSet<>(); + private final CompositeDisposable disposables = new CompositeDisposable(); private enum FetchEnd { TOP, @@ -179,7 +179,9 @@ public class NotificationsFragment extends SFragment implements @Override public NotificationViewData apply(Either input) { if (input.isRight()) { - Notification notification = input.asRight(); + Notification notification = input.asRight() + .rewriteToStatusTypeIfNeeded(accountManager.getActiveAccount().getAccountId()); + return ViewDataUtils.notificationToViewData( notification, alwaysShowSensitiveMedia, @@ -249,7 +251,9 @@ public class NotificationsFragment extends SFragment implements preferences.getBoolean("showBotOverlay", true), preferences.getBoolean("useBlurhash", true), CardViewMode.NONE, - preferences.getBoolean("confirmReblogs", true) + preferences.getBoolean("confirmReblogs", true), + preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) ); adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(), @@ -681,32 +685,21 @@ public class NotificationsFragment extends SFragment implements updateAdapter(); //Execute clear notifications request - Call call = mastodonApi.clearNotifications(); - call.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (isAdded()) { - if (!response.isSuccessful()) { - //Reload notifications on failure - fullyRefreshWithProgressBar(true); - } - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - //Reload notifications on failure - fullyRefreshWithProgressBar(true); - } - }); - callList.add(call); + mastodonApi.clearNotifications() + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + response -> { + // nothing to do + }, + throwable -> { + //Reload notifications on failure + fullyRefreshWithProgressBar(true); + }); } private void resetNotificationsLoad() { - for (Call callItem : callList) { - callItem.cancel(); - } - callList.clear(); + disposables.clear(); bottomLoading = false; topLoading = false; @@ -770,6 +763,8 @@ public class NotificationsFragment extends SFragment implements return getString(R.string.notification_follow_request_name); case POLL: return getString(R.string.notification_poll_name); + case STATUS: + return getString(R.string.notification_subscription_name); default: return "Unknown"; } @@ -797,6 +792,7 @@ public class NotificationsFragment extends SFragment implements private void loadNotificationsFilter() { AccountEntity account = accountManager.getActiveAccount(); if (account != null) { + notificationFilter.clear(); notificationFilter.addAll(NotificationTypeConverterKt.deserialize( account.getNotificationsFilter())); } @@ -833,8 +829,8 @@ public class NotificationsFragment extends SFragment implements @Override public void onRespondToFollowRequest(boolean accept, String id, int position) { Single request = accept ? - mastodonApi.authorizeFollowRequestObservable(id) : - mastodonApi.rejectFollowRequestObservable(id); + mastodonApi.authorizeFollowRequest(id) : + mastodonApi.rejectFollowRequest(id); request.observeOn(AndroidSchedulers.mainThread()) .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe( @@ -952,27 +948,20 @@ public class NotificationsFragment extends SFragment implements bottomLoading = true; } - Call> call = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE, showNotificationsFilter ? notificationFilter : null); - - call.enqueue(new Callback>() { - @Override - public void onResponse(@NonNull Call> call, - @NonNull Response> response) { - if (response.isSuccessful()) { - String linkHeader = response.headers().get("Link"); - onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos); - } else { - onFetchNotificationsFailure(new Exception(response.message()), fetchEnd, pos); - } - } - - @Override - public void onFailure(@NonNull Call> call, @NonNull Throwable t) { - if (!call.isCanceled()) - onFetchNotificationsFailure((Exception) t, fetchEnd, pos); - } - }); - callList.add(call); + Disposable notificationCall = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE, showNotificationsFilter ? notificationFilter : null) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + response -> { + if (response.isSuccessful()) { + String linkHeader = response.headers().get("Link"); + onFetchNotificationsSuccess(response.body(), linkHeader, fetchEnd, pos); + } else { + onFetchNotificationsFailure(new Exception(response.message()), fetchEnd, pos); + } + }, + throwable -> onFetchNotificationsFailure(throwable, fetchEnd, pos)); + disposables.add(notificationCall); } private void onFetchNotificationsSuccess(List notifications, String linkHeader, @@ -1031,7 +1020,7 @@ public class NotificationsFragment extends SFragment implements progressBar.setVisibility(View.GONE); } - private void onFetchNotificationsFailure(Exception exception, FetchEnd fetchEnd, int position) { + private void onFetchNotificationsFailure(Throwable throwable, FetchEnd fetchEnd, int position) { swipeRefreshLayout.setRefreshing(false); if (fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) { Placeholder placeholder = notifications.get(position).asLeft(); @@ -1043,7 +1032,7 @@ public class NotificationsFragment extends SFragment implements this.statusView.setVisibility(View.VISIBLE); swipeRefreshLayout.setEnabled(false); this.showingError = true; - if (exception instanceof IOException) { + if (throwable instanceof IOException) { this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { this.progressBar.setVisibility(View.VISIBLE); this.onRefresh(); @@ -1058,7 +1047,7 @@ public class NotificationsFragment extends SFragment implements } updateFilterVisibility(); } - Log.e(TAG, "Fetch failure: " + exception.getMessage()); + Log.e(TAG, "Fetch failure: " + throwable.getMessage()); if (fetchEnd == FetchEnd.TOP) { topLoading = false; @@ -1273,6 +1262,12 @@ public class NotificationsFragment extends SFragment implements @Override public void onResume() { super.onResume(); + String rawAccountNotificationFilter = accountManager.getActiveAccount().getNotificationsFilter(); + Set accountNotificationFilter = NotificationTypeConverterKt.deserialize(rawAccountNotificationFilter); + if (!notificationFilter.equals(accountNotificationFilter)) { + loadNotificationsFilter(); + fullyRefreshWithProgressBar(true); + } startUpdateTimestamp(); } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index 8fe141c6..9d4b45b3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -20,7 +20,6 @@ import android.app.DownloadManager; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageManager; import android.net.Uri; @@ -30,8 +29,6 @@ import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.widget.CheckBox; -import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; @@ -41,14 +38,14 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.PopupMenu; import androidx.core.app.ActivityOptionsCompat; import androidx.core.view.ViewCompat; +import androidx.fragment.app.Fragment; import androidx.lifecycle.Lifecycle; -import androidx.preference.PreferenceManager; import com.keylesspalace.tusky.BaseActivity; import com.keylesspalace.tusky.BottomSheetActivity; import com.keylesspalace.tusky.MainActivity; -import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.PostLookupFallbackBehavior; +import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.ViewMediaActivity; import com.keylesspalace.tusky.ViewTagActivity; import com.keylesspalace.tusky.components.compose.ComposeActivity; @@ -63,6 +60,7 @@ import com.keylesspalace.tusky.entity.PollOption; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.TimelineCases; +import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.view.MuteAccountDialog; import com.keylesspalace.tusky.viewdata.AttachmentViewData; @@ -75,9 +73,8 @@ import java.util.regex.Pattern; import javax.inject.Inject; -import kotlin.Unit; - import io.reactivex.android.schedulers.AndroidSchedulers; +import kotlin.Unit; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -91,7 +88,7 @@ import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvid * adapters. I feel like the profile pages and thread viewer, which I haven't made yet, will also * overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear * up what needs to be where. */ -public abstract class SFragment extends BaseFragment implements Injectable { +public abstract class SFragment extends Fragment implements Injectable { protected abstract void removeItem(int position); @@ -102,7 +99,7 @@ public abstract class SFragment extends BaseFragment implements Injectable { private static List filters; private boolean filterRemoveRegex; private Matcher filterRemoveRegexMatcher; - private static Matcher alphanumeric = Pattern.compile("^\\w+$").matcher(""); + private static final Matcher alphanumeric = Pattern.compile("^\\w+$").matcher(""); @Inject public MastodonApi mastodonApi; @@ -340,8 +337,8 @@ public abstract class SFragment extends BaseFragment implements Injectable { MuteAccountDialog.showMuteAccountDialog( this.getActivity(), accountUsername, - (notifications) -> { - timelineCases.mute(accountId, notifications); + (notifications, duration) -> { + timelineCases.mute(accountId, notifications, duration); return Unit.INSTANCE; } ); @@ -395,10 +392,9 @@ public abstract class SFragment extends BaseFragment implements Injectable { } break; } + default: case UNKNOWN: { - /* Intentionally do nothing. This case is here is to handle when new attachment - * types are added to the API before code is added here to handle them. So, the - * best fallback is to just show the preview and ignore requests to view them. */ + LinkHelper.openLink(active.getUrl(), getContext()); break; } } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index b48b1b45..4f6a2e1b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -24,11 +24,13 @@ import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.accessibility.AccessibilityManager; import android.widget.ProgressBar; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.arch.core.util.Function; +import androidx.core.content.ContextCompat; import androidx.core.util.Pair; import androidx.core.widget.ContentLoadingProgressBar; import androidx.lifecycle.Lifecycle; @@ -74,6 +76,7 @@ import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.repository.Placeholder; import com.keylesspalace.tusky.repository.TimelineRepository; import com.keylesspalace.tusky.repository.TimelineRequestMode; +import com.keylesspalace.tusky.settings.PrefKeys; import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.Either; import com.keylesspalace.tusky.util.HttpHeaderLink; @@ -83,7 +86,6 @@ import com.keylesspalace.tusky.util.ListUtils; import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.util.StringUtils; -import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ViewDataUtils; import com.keylesspalace.tusky.view.BackgroundMessageView; import com.keylesspalace.tusky.view.EndlessOnScrollListener; @@ -95,18 +97,18 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.ListIterator; +import java.util.Objects; import java.util.concurrent.TimeUnit; import javax.inject.Inject; import at.connyduck.sparkbutton.helpers.Utils; import io.reactivex.Observable; +import io.reactivex.Single; import io.reactivex.android.schedulers.AndroidSchedulers; import kotlin.Unit; import kotlin.collections.CollectionsKt; import kotlin.jvm.functions.Function1; -import retrofit2.Call; -import retrofit2.Callback; import retrofit2.Response; import static com.uber.autodispose.AutoDispose.autoDisposable; @@ -252,7 +254,9 @@ public class TimelineFragment extends SFragment implements preferences.getBoolean("showCardsInTimelines", false) ? CardViewMode.INDENTED : CardViewMode.NONE, - preferences.getBoolean("confirmReblogs", true) + preferences.getBoolean("confirmReblogs", true), + preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) ); adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this); @@ -1003,7 +1007,7 @@ public class TimelineFragment extends SFragment implements } } - private Call> getFetchCallByTimelineType(String fromId, String uptoId) { + private Single>> getFetchCallByTimelineType(String fromId, String uptoId) { MastodonApi api = mastodonApi; switch (kind) { default: @@ -1050,37 +1054,31 @@ public class TimelineFragment extends SFragment implements .observeOn(AndroidSchedulers.mainThread()) .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) .subscribe( - (result) -> onFetchTimelineSuccess(result, fetchEnd, pos), - (err) -> onFetchTimelineFailure(new Exception(err), fetchEnd, pos) + result -> onFetchTimelineSuccess(result, fetchEnd, pos), + err -> onFetchTimelineFailure(err, fetchEnd, pos) ); } else { - Callback> callback = new Callback>() { - @Override - public void onResponse(@NonNull Call> call, @NonNull Response> response) { - if (response.isSuccessful()) { - @Nullable - String newNextId = extractNextId(response); - if (newNextId != null) { - // when we reach the bottom of the list, we won't have a new link. If - // we blindly write `null` here we will start loading from the top - // again. - nextId = newNextId; - } - onFetchTimelineSuccess(liftStatusList(response.body()), fetchEnd, pos); - } else { - onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos); - } - } - - @Override - public void onFailure(@NonNull Call> call, @NonNull Throwable t) { - onFetchTimelineFailure((Exception) t, fetchEnd, pos); - } - }; - - Call> listCall = getFetchCallByTimelineType(maxId, sinceId); - callList.add(listCall); - listCall.enqueue(callback); + getFetchCallByTimelineType(maxId, sinceId) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + response -> { + if (response.isSuccessful()) { + @Nullable + String newNextId = extractNextId(response); + if (newNextId != null) { + // when we reach the bottom of the list, we won't have a new link. If + // we blindly write `null` here we will start loading from the top + // again. + nextId = newNextId; + } + onFetchTimelineSuccess(liftStatusList(response.body()), fetchEnd, pos); + } else { + onFetchTimelineFailure(new Exception(response.message()), fetchEnd, pos); + } + }, + err -> onFetchTimelineFailure(err, fetchEnd, pos) + ); } } @@ -1157,7 +1155,7 @@ public class TimelineFragment extends SFragment implements } } - private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) { + private void onFetchTimelineFailure(Throwable throwable, FetchEnd fetchEnd, int position) { if (isAdded()) { swipeRefreshLayout.setRefreshing(false); topProgressBar.hide(); @@ -1176,7 +1174,7 @@ public class TimelineFragment extends SFragment implements } else if (this.statuses.isEmpty()) { swipeRefreshLayout.setEnabled(false); this.statusView.setVisibility(View.VISIBLE); - if (exception instanceof IOException) { + if (throwable instanceof IOException) { this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> { this.progressBar.setVisibility(View.VISIBLE); this.onRefresh(); @@ -1191,7 +1189,7 @@ public class TimelineFragment extends SFragment implements } } - Log.e(TAG, "Fetch Failure: " + exception.getMessage()); + Log.e(TAG, "Fetch Failure: " + throwable.getMessage()); updateBottomLoadingState(fetchEnd); progressBar.setVisibility(View.GONE); } @@ -1476,9 +1474,21 @@ public class TimelineFragment extends SFragment implements } }; + AccessibilityManager a11yManager; + boolean talkBackWasEnabled; + @Override public void onResume() { super.onResume(); + a11yManager = Objects.requireNonNull( + ContextCompat.getSystemService(requireContext(), AccessibilityManager.class) + ); + boolean wasEnabled = this.talkBackWasEnabled; + talkBackWasEnabled = a11yManager.isEnabled(); + Log.d(TAG, "talkback was enabled: " + wasEnabled + ", now " + talkBackWasEnabled); + if (talkBackWasEnabled && !wasEnabled) { + this.adapter.notifyDataSetChanged(); + } startUpdateTimestamp(); } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt index 86b3d09b..b25fec26 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt @@ -17,10 +17,11 @@ package com.keylesspalace.tusky.fragment import android.os.Bundle import android.text.TextUtils +import androidx.fragment.app.Fragment import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.entity.Attachment -abstract class ViewMediaFragment : BaseFragment() { +abstract class ViewMediaFragment : Fragment() { private var toolbarVisibiltyDisposable: Function0? = null abstract fun setupMediaView( diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index 7186c552..ccd0c3d4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -55,14 +55,13 @@ import com.keylesspalace.tusky.di.Injectable; import com.keylesspalace.tusky.entity.Filter; import com.keylesspalace.tusky.entity.Poll; import com.keylesspalace.tusky.entity.Status; -import com.keylesspalace.tusky.entity.StatusContext; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.network.MastodonApi; +import com.keylesspalace.tusky.settings.PrefKeys; import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate; import com.keylesspalace.tusky.util.PairedList; import com.keylesspalace.tusky.util.StatusDisplayOptions; -import com.keylesspalace.tusky.util.ThemeUtils; import com.keylesspalace.tusky.util.ViewDataUtils; import com.keylesspalace.tusky.view.ConversationLineItemDecoration; import com.keylesspalace.tusky.viewdata.StatusViewData; @@ -75,9 +74,6 @@ import java.util.Locale; import javax.inject.Inject; import io.reactivex.android.schedulers.AndroidSchedulers; -import retrofit2.Call; -import retrofit2.Callback; -import retrofit2.Response; import static com.uber.autodispose.AutoDispose.autoDisposable; import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from; @@ -127,6 +123,7 @@ public final class ViewThreadFragment extends SFragment implements thisThreadsStatusId = getArguments().getString("id"); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); + StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions( preferences.getBoolean("animateGifAvatars", false), accountManager.getActiveAccount().getMediaPreviewEnabled(), @@ -136,7 +133,9 @@ public final class ViewThreadFragment extends SFragment implements preferences.getBoolean("showCardsInTimelines", false) ? CardViewMode.INDENTED : CardViewMode.NONE, - preferences.getBoolean("confirmReblogs", true) + preferences.getBoolean("confirmReblogs", true), + preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) ); adapter = new ThreadAdapter(statusDisplayOptions, this); } @@ -461,49 +460,32 @@ public final class ViewThreadFragment extends SFragment implements } private void sendStatusRequest(final String id) { - Call call = mastodonApi.status(id); - call.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - if (response.isSuccessful()) { - int position = setStatus(response.body()); - recyclerView.scrollToPosition(position); - } else { - onThreadRequestFailure(id); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - onThreadRequestFailure(id); - } - }); - callList.add(call); + mastodonApi.status(id) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + status -> { + int position = setStatus(status); + recyclerView.scrollToPosition(position); + }, + throwable -> onThreadRequestFailure(id, throwable) + ); } private void sendThreadRequest(final String id) { - Call call = mastodonApi.statusContext(id); - call.enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - StatusContext context = response.body(); - if (response.isSuccessful() && context != null) { - swipeRefreshLayout.setRefreshing(false); - setContext(context.getAncestors(), context.getDescendants()); - } else { - onThreadRequestFailure(id); - } - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - onThreadRequestFailure(id); - } - }); - callList.add(call); + mastodonApi.statusContext(id) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe( + context -> { + swipeRefreshLayout.setRefreshing(false); + setContext(context.getAncestors(), context.getDescendants()); + }, + throwable -> onThreadRequestFailure(id, throwable) + ); } - private void onThreadRequestFailure(final String id) { + private void onThreadRequestFailure(final String id, final Throwable throwable) { View view = getView(); swipeRefreshLayout.setRefreshing(false); if (view != null) { @@ -514,7 +496,7 @@ public final class ViewThreadFragment extends SFragment implements }) .show(); } else { - Log.e(TAG, "Couldn't display thread fetch error message"); + Log.e(TAG, "Network request failed", throwable); } } diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 8f3dab3f..28ac77b6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -56,14 +56,7 @@ interface MastodonApi { @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> - - @GET("api/v1/timelines/home") - fun homeTimelineSingle( - @Query("max_id") maxId: String?, - @Query("since_id") sinceId: String?, - @Query("limit") limit: Int? - ): Single> + ): Single>> @GET("api/v1/timelines/public") fun publicTimeline( @@ -71,7 +64,7 @@ interface MastodonApi { @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> + ): Single>> @GET("api/v1/timelines/tag/{hashtag}") fun hashtagTimeline( @@ -81,7 +74,7 @@ interface MastodonApi { @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> + ): Single>> @GET("api/v1/timelines/list/{listId}") fun listTimeline( @@ -89,7 +82,7 @@ interface MastodonApi { @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> + ): Single>> @GET("api/v1/notifications") fun notifications( @@ -97,7 +90,7 @@ interface MastodonApi { @Query("since_id") sinceId: String?, @Query("limit") limit: Int?, @Query("exclude_types[]") excludes: Set? - ): Call> + ): Single>> @GET("api/v1/markers") fun markersWithAuth( @@ -114,17 +107,13 @@ interface MastodonApi { ): Single> @POST("api/v1/notifications/clear") - fun clearNotifications(): Call - - @GET("api/v1/notifications/{id}") - fun notification( - @Path("id") notificationId: String - ): Call + fun clearNotifications(): Single @Multipart @POST("api/v1/media") fun uploadMedia( - @Part file: MultipartBody.Part + @Part file: MultipartBody.Part, + @Part description: MultipartBody.Part? = null ): Single @FormUrlEncoded @@ -145,12 +134,12 @@ interface MastodonApi { @GET("api/v1/statuses/{id}") fun status( @Path("id") statusId: String - ): Call + ): Single @GET("api/v1/statuses/{id}/context") fun statusContext( @Path("id") statusId: String - ): Call + ): Single @GET("api/v1/statuses/{id}/reblogged_by") fun statusRebloggedBy( @@ -289,7 +278,7 @@ interface MastodonApi { @Query("exclude_replies") excludeReplies: Boolean?, @Query("only_media") onlyMedia: Boolean?, @Query("pinned") pinned: Boolean? - ): Call> + ): Single>> @GET("api/v1/accounts/{id}/followers") fun accountFollowers( @@ -307,7 +296,8 @@ interface MastodonApi { @POST("api/v1/accounts/{id}/follow") fun followAccount( @Path("id") accountId: String, - @Field("reblogs") showReblogs: Boolean + @Field("reblogs") showReblogs: Boolean? = null, + @Field("notify") notify: Boolean? = null ): Single @POST("api/v1/accounts/{id}/unfollow") @@ -329,7 +319,8 @@ interface MastodonApi { @POST("api/v1/accounts/{id}/mute") fun muteAccount( @Path("id") accountId: String, - @Field("notifications") notifications: Boolean? = null + @Field("notifications") notifications: Boolean? = null, + @Field("duration") duration: Int? = null ): Single @POST("api/v1/accounts/{id}/unmute") @@ -347,6 +338,16 @@ interface MastodonApi { @Path("id") accountId: String ): Single> + @POST("api/v1/pleroma/accounts/{id}/subscribe") + fun subscribeAccount( + @Path("id") accountId: String + ): Single + + @POST("api/v1/pleroma/accounts/{id}/unsubscribe") + fun unsubscribeAccount( + @Path("id") accountId: String + ): Single + @GET("api/v1/blocks") fun blocks( @Query("max_id") maxId: String? @@ -380,14 +381,14 @@ interface MastodonApi { @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> + ): Single>> @GET("api/v1/bookmarks") fun bookmarks( @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Call> + ): Single>> @GET("api/v1/follow_requests") fun followRequests( @@ -397,20 +398,10 @@ interface MastodonApi { @POST("api/v1/follow_requests/{id}/authorize") fun authorizeFollowRequest( @Path("id") accountId: String - ): Call - - @POST("api/v1/follow_requests/{id}/reject") - fun rejectFollowRequest( - @Path("id") accountId: String - ): Call - - @POST("api/v1/follow_requests/{id}/authorize") - fun authorizeFollowRequestObservable( - @Path("id") accountId: String ): Single @POST("api/v1/follow_requests/{id}/reject") - fun rejectFollowRequestObservable( + fun rejectFollowRequest( @Path("id") accountId: String ): Single diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt index efdb410b..8cf2b688 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt @@ -33,7 +33,7 @@ interface TimelineCases { fun reblog(status: Status, reblog: Boolean): Single fun favourite(status: Status, favourite: Boolean): Single fun bookmark(status: Status, bookmark: Boolean): Single - fun mute(id: String, notifications: Boolean) + fun mute(id: String, notifications: Boolean, duration: Int) fun block(id: String) fun delete(id: String): Single fun pin(status: Status, pin: Boolean) @@ -104,8 +104,8 @@ class TimelineCasesImpl( } } - override fun mute(id: String, notifications: Boolean) { - mastodonApi.muteAccount(id, notifications) + override fun mute(id: String, notifications: Boolean, duration: Int) { + mastodonApi.muteAccount(id, notifications, duration) .subscribe({ eventHub.dispatch(MuteEvent(id)) }, { t -> diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt index 4e1b92f5..db861701 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -60,7 +60,6 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { val notificationManager = NotificationManagerCompat.from(context) - if (intent.action == NotificationHelper.REPLY_ACTION) { val message = getReplyMessage(intent) @@ -89,22 +88,23 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { val sendIntent = SendTootService.sendTootIntent( context, TootToSend( - text, - spoiler, - visibility.serverString(), - false, - emptyList(), - emptyList(), - emptyList(), - null, - citedStatusId, - null, - null, - null, - null, account.id, - 0, - randomAlphanumericString(16), - 0 + text = text, + warningText = spoiler, + visibility = visibility.serverString(), + sensitive = false, + mediaIds = emptyList(), + mediaUris = emptyList(), + mediaDescriptions = emptyList(), + scheduledAt = null, + inReplyToId = citedStatusId, + poll = null, + replyingStatusContent = null, + replyingStatusAuthorUsername = null, + accountId = account.id, + savedTootUid = -1, + draftId = -1, + idempotencyKey = randomAlphanumericString(16), + retries = 0 ) ) diff --git a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt index 8aa3eb76..945c55d3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt @@ -66,9 +66,9 @@ class TimelineRepositoryImpl( sinceIdMinusOne: String?, limit: Int, accountId: Long, requestMode: TimelineRequestMode ): Single> { - return mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1) - .map { statuses -> - this.saveStatusesToDb(accountId, statuses, maxId, sinceId) + return mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1) + .map { response -> + this.saveStatusesToDb(accountId, response.body().orEmpty(), maxId, sinceId) } .flatMap { statuses -> this.addFromDbIfNeeded(accountId, statuses, maxId, sinceId, limit, requestMode) @@ -85,7 +85,7 @@ class TimelineRepositoryImpl( private fun addFromDbIfNeeded(accountId: Long, statuses: List>, maxId: String?, sinceId: String?, limit: Int, requestMode: TimelineRequestMode - ): Single>? { + ): Single> { return if (requestMode != NETWORK && statuses.size < 2) { val newMaxID = if (statuses.isEmpty()) { maxId diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt index ff88239b..e427cf43 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendTootService.kt @@ -18,6 +18,7 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.StatusComposedEvent import com.keylesspalace.tusky.appstore.StatusScheduledEvent +import com.keylesspalace.tusky.components.drafts.DraftHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.di.Injectable @@ -46,7 +47,8 @@ class SendTootService : Service(), Injectable { lateinit var eventHub: EventHub @Inject lateinit var database: AppDatabase - + @Inject + lateinit var draftHelper: DraftHelper @Inject lateinit var saveTootHelper: SaveTootHelper @@ -163,6 +165,10 @@ class SendTootService : Service(), Injectable { if (tootToSend.savedTootUid != 0) { saveTootHelper.deleteDraft(tootToSend.savedTootUid) } + if (tootToSend.draftId != 0) { + draftHelper.deleteDraftAndAttachments(tootToSend.draftId) + .subscribe() + } if (scheduled) { response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch) @@ -245,17 +251,19 @@ class SendTootService : Service(), Injectable { private fun saveTootToDrafts(toot: TootToSend) { - saveTootHelper.saveToot(toot.text, - toot.warningText, - toot.savedJsonUrls, - toot.mediaUris, - toot.mediaDescriptions, - toot.savedTootUid, - toot.inReplyToId, - toot.replyingStatusContent, - toot.replyingStatusAuthorUsername, - Status.Visibility.byString(toot.visibility), - toot.poll) + draftHelper.saveDraft( + draftId = toot.draftId, + accountId = toot.accountId, + inReplyToId = toot.inReplyToId, + content = toot.text, + contentWarning = toot.warningText, + sensitive = toot.sensitive, + visibility = Status.Visibility.byString(toot.visibility), + mediaUris = toot.mediaUris, + mediaDescriptions = toot.mediaDescriptions, + poll = toot.poll, + failedToSend = true + ).subscribe() } private fun cancelSendingIntent(tootId: Int): PendingIntent { @@ -323,9 +331,9 @@ data class TootToSend( val poll: NewPoll?, val replyingStatusContent: String?, val replyingStatusAuthorUsername: String?, - val savedJsonUrls: List?, val accountId: Long, val savedTootUid: Int, + val draftId: Int, val idempotencyKey: String, var retries: Int ) : Parcelable diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt index df9627d8..d014ec0c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt @@ -31,8 +31,12 @@ object PrefKeys { const val SHOW_CARDS_IN_TIMELINES = "showCardsInTimelines" const val CONFIRM_REBLOGS = "confirmReblogs" const val ENABLE_SWIPE_FOR_TABS = "enableSwipeForTabs" + const val ANIMATE_CUSTOM_EMOJIS = "animateCustomEmojis" const val CUSTOM_TABS = "customTabs" + const val WELLBEING_LIMITED_NOTIFICATIONS = "wellbeingModeLimitedNotifications" + const val WELLBEING_HIDE_STATS_POSTS = "wellbeingHideStatsPosts" + const val WELLBEING_HIDE_STATS_PROFILE = "wellbeingHideStatsProfile" const val HTTP_PROXY_ENABLED = "httpProxyEnabled" const val HTTP_PROXY_SERVER = "httpProxyServer" @@ -53,6 +57,7 @@ object PrefKeys { const val NOTIFICATION_FILTER_REBLOGS = "notificationFilterReblogs" const val NOTIFICATION_FILTER_FOLLOW_REQUESTS = "notificationFilterFollowRequests" const val NOTIFICATIONS_FILTER_FOLLOWS = "notificationFilterFollows" + const val NOTIFICATION_FILTER_SUBSCRIPTIONS = "notificationFilterSubscriptions" const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies" const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts" diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt new file mode 100644 index 00000000..14aee81b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/BindingViewHolder.kt @@ -0,0 +1,8 @@ +package com.keylesspalace.tusky.util + +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding + +class BindingViewHolder( + val binding: T +) : RecyclerView.ViewHolder(binding.root) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt b/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt index a1294333..c0da4275 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ComposeTokenizer.kt @@ -21,20 +21,52 @@ import android.text.TextUtils import android.widget.MultiAutoCompleteTextView class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer { + + private fun isMentionOrHashtagAllowedCharacter(character: Char) : Boolean { + return Character.isLetterOrDigit(character) || character == '_' // simple usernames + || character == '-' // extended usernames + || character == '.' // domain dot + } + override fun findTokenStart(text: CharSequence, cursor: Int): Int { if (cursor == 0) { return cursor } var i = cursor var character = text[i - 1] - while (i > 0 && character != '@' && character != '#' && character != ':') { - // See SpanUtils.MENTION_REGEX - if (!Character.isLetterOrDigit(character) && character != '_') { + + // go up to first illegal character or character we're looking for (@, # or :) + while(i > 0 && !(character == '@' || character == '#' || character == ':')) { + if(!isMentionOrHashtagAllowedCharacter(character)) { return cursor } + i-- character = if (i == 0) ' ' else text[i - 1] } + + // maybe caught domain name? try search username + if(i > 2 && character == '@') { + var j = i - 1 + var character2 = text[i - 2] + + // again go up to first illegal character or tag "@" + while(j > 0 && character2 != '@') { + if(!isMentionOrHashtagAllowedCharacter(character2)) { + break + } + + j-- + character2 = if (j == 0) ' ' else text[j - 1] + } + + // found mention symbol, override cursor + if(character2 == '@') { + i = j + character = character2 + } + } + if (i < 1 || (character != '@' && character != '#' && character != ':') || i > 1 && !Character.isWhitespace(text[i - 2])) { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt index 679f38d3..7521afe4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt @@ -16,11 +16,9 @@ @file:JvmName("CustomEmojiHelper") package com.keylesspalace.tusky.util -import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Paint -import android.graphics.drawable.BitmapDrawable -import android.graphics.drawable.Drawable +import android.graphics.drawable.* import android.text.SpannableStringBuilder import android.text.style.ReplacementSpan import android.view.View @@ -33,6 +31,8 @@ import com.keylesspalace.tusky.entity.Emoji import java.lang.ref.WeakReference import java.util.regex.Pattern +import androidx.preference.PreferenceManager +import com.keylesspalace.tusky.settings.PrefKeys /** * replaces emoji shortcodes in a text with EmojiSpans @@ -41,7 +41,7 @@ import java.util.regex.Pattern * @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable) * @return the text with the shortcodes replaced by EmojiSpans */ -fun CharSequence.emojify(emojis: List?, view: View) : CharSequence { +fun CharSequence.emojify(emojis: List?, view: View, animate: Boolean) : CharSequence { if(emojis.isNullOrEmpty()) return this @@ -56,9 +56,9 @@ fun CharSequence.emojify(emojis: List?, view: View) : CharSequence { builder.setSpan(span, matcher.start(), matcher.end(), 0) Glide.with(view) - .asBitmap() + .asDrawable() .load(url) - .into(span.getTarget()) + .into(span.getTarget(animate)) } } return builder @@ -97,11 +97,29 @@ class EmojiSpan(val viewWeakReference: WeakReference) : ReplacementSpan() } } - fun getTarget(): Target { - return object : CustomTarget() { - override fun onResourceReady(resource: Bitmap, transition: Transition?) { + fun getTarget(animate : Boolean): Target { + return object : CustomTarget() { + override fun onResourceReady(resource: Drawable, transition: Transition?) { viewWeakReference.get()?.let { view -> - imageDrawable = BitmapDrawable(view.context.resources, resource) + if(animate && resource is Animatable) { + val callback = resource.callback + + resource.callback = object: Drawable.Callback { + override fun unscheduleDrawable(p0: Drawable, p1: Runnable) { + callback?.unscheduleDrawable(p0, p1) + } + override fun scheduleDrawable(p0: Drawable, p1: Runnable, p2: Long) { + callback?.scheduleDrawable(p0, p1, p2) + } + override fun invalidateDrawable(p0: Drawable) { + callback?.invalidateDrawable(p0) + view.invalidate() + } + } + resource.start() + } + + imageDrawable = resource view.invalidate() } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java index 64e329c8..ec0c8a3e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.java @@ -19,7 +19,6 @@ import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.method.LinkMovementMethod; @@ -31,6 +30,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.browser.customtabs.CustomTabColorSchemeParams; import androidx.browser.customtabs.CustomTabsIntent; import androidx.preference.PreferenceManager; @@ -229,18 +229,20 @@ public class LinkHelper { */ public static void openLinkInCustomTab(Uri uri, Context context) { int toolbarColor = ThemeUtils.getColor(context, R.attr.colorSurface); + int navigationbarColor = ThemeUtils.getColor(context, android.R.attr.navigationBarColor); + int navigationbarDividerColor = ThemeUtils.getColor(context, R.attr.dividerColor); - CustomTabsIntent.Builder customTabsIntentBuilder = new CustomTabsIntent.Builder() + CustomTabColorSchemeParams colorSchemeParams = new CustomTabColorSchemeParams.Builder() .setToolbarColor(toolbarColor) - .setShowTitle(true); + .setNavigationBarColor(navigationbarColor) + .setNavigationBarDividerColor(navigationbarDividerColor) + .build(); - if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - customTabsIntentBuilder.setNavigationBarColor( - ThemeUtils.getColor(context, android.R.attr.navigationBarColor) - ); - } + CustomTabsIntent customTabsIntent = new CustomTabsIntent.Builder() + .setDefaultColorSchemeParams(colorSchemeParams) + .setShowTitle(true) + .build(); - CustomTabsIntent customTabsIntent = customTabsIntentBuilder.build(); try { customTabsIntent.launchUrl(context, uri); } catch (ActivityNotFoundException e) { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.kt deleted file mode 100644 index 3e1b89c6..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/OkHttpUtils.kt +++ /dev/null @@ -1,115 +0,0 @@ -/* Copyright 2020 Tusky Contributors - * - * This file is part of Tusky. - * - * Tusky is free software: you can redistribute it and/or modify it under the terms of the GNU - * Lesser 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 Lesser - * General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License along with Tusky. If - * not, see . */ - -package com.keylesspalace.tusky.util - -import android.content.Context -import android.os.Build -import androidx.preference.PreferenceManager -import com.keylesspalace.tusky.BuildConfig -import okhttp3.Cache -import okhttp3.OkHttp -import okhttp3.OkHttpClient -import okhttp3.tls.HandshakeCertificates -import java.io.ByteArrayInputStream -import java.net.InetSocketAddress -import java.net.Proxy -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit - -fun okhttpClient(context: Context): OkHttpClient.Builder { - val preferences = PreferenceManager.getDefaultSharedPreferences(context) - - val httpProxyEnabled = preferences.getBoolean("httpProxyEnabled", false) - val httpServer = preferences.getNonNullString("httpProxyServer", "") - val httpPort = preferences.getNonNullString("httpProxyPort", "-1").toIntOrNull() ?: -1 - - val cacheSize = 25 * 1024 * 1024 // 25 MiB - val builder = OkHttpClient.Builder() - .addInterceptor { chain -> - /** - * Add a custom User-Agent that contains Tusky, Android and Okhttp Version to all requests - * Example: - * User-Agent: Tusky/1.1.2 Android/5.0.2 - * */ - val requestWithUserAgent = chain.request().newBuilder() - .header( - "User-Agent", - "Tusky/${BuildConfig.VERSION_NAME} Android/${Build.VERSION.RELEASE} OkHttp/${OkHttp.VERSION}" - ) - .build() - chain.proceed(requestWithUserAgent) - } - .readTimeout(30, TimeUnit.SECONDS) - .writeTimeout(30, TimeUnit.SECONDS) - .cache(Cache(context.cacheDir, cacheSize.toLong())) - - if (httpProxyEnabled && httpServer.isNotEmpty() && httpPort > 0 && httpPort < 65535) { - val address = InetSocketAddress.createUnresolved(httpServer, httpPort) - builder.proxy(Proxy(Proxy.Type.HTTP, address)) - } - - // trust the new Let's Encrypt root certificate that is not available on Android < 7.1.1 - // new cert https://letsencrypt.org/certs/isrgrootx1.pem - // see https://letsencrypt.org/2020/11/06/own-two-feet.html - // see https://stackoverflow.com/questions/64844311/certpathvalidatorexception-connecting-to-a-lets-encrypt-host-on-android-m-or-ea - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - val isgCert = """ - -----BEGIN CERTIFICATE----- - MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw - TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh - cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 - WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu - ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY - MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc - h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ - 0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U - A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW - T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH - B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC - B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv - KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn - OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn - jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw - qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI - rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV - HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq - hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL - ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ - 3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK - NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 - ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur - TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC - jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc - oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq - 4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA - mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d - emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= - -----END CERTIFICATE----- - """.trimIndent() - val cf = CertificateFactory.getInstance("X.509") - val isgCertificate = cf.generateCertificate(ByteArrayInputStream(isgCert.toByteArray(charset("UTF-8")))) - val certificates = HandshakeCertificates.Builder() - .addTrustedCertificate(isgCertificate as X509Certificate) - .addPlatformTrustedCertificates() - .build() - builder.sslSocketFactory( - certificates.sslSocketFactory(), - certificates.trustManager - ) - } - return builder -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt index 2ad4b825..c78b0f78 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/RxAwareViewModel.kt @@ -1,5 +1,6 @@ package com.keylesspalace.tusky.util +import androidx.annotation.CallSuper import androidx.lifecycle.ViewModel import io.reactivex.disposables.CompositeDisposable import io.reactivex.disposables.Disposable @@ -9,6 +10,7 @@ open class RxAwareViewModel : ViewModel() { fun Disposable.autoDispose() = disposables.add(this) + @CallSuper override fun onCleared() { super.onCleared() disposables.clear() diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java index 69009830..29693550 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/SaveTootHelper.java @@ -1,33 +1,18 @@ package com.keylesspalace.tusky.util; -import android.annotation.SuppressLint; -import android.content.ContentResolver; import android.content.Context; import android.net.Uri; -import android.os.AsyncTask; -import android.text.TextUtils; import android.util.Log; -import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.FileProvider; import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; -import com.keylesspalace.tusky.BuildConfig; import com.keylesspalace.tusky.db.AppDatabase; import com.keylesspalace.tusky.db.TootDao; import com.keylesspalace.tusky.db.TootEntity; -import com.keylesspalace.tusky.entity.NewPoll; -import com.keylesspalace.tusky.entity.Status; -import java.io.File; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Locale; import javax.inject.Inject; @@ -45,61 +30,6 @@ public final class SaveTootHelper { this.context = context; } - @SuppressLint("StaticFieldLeak") - public boolean saveToot(@NonNull String content, - @NonNull String contentWarning, - @Nullable List savedJsonUrls, - @NonNull List mediaUris, - @NonNull List mediaDescriptions, - int savedTootUid, - @Nullable String inReplyToId, - @Nullable String replyingStatusContent, - @Nullable String replyingStatusAuthorUsername, - @NonNull Status.Visibility statusVisibility, - @Nullable NewPoll poll) { - - if (TextUtils.isEmpty(content) && mediaUris.isEmpty() && poll == null) { - return false; - } - - // Get any existing file's URIs. - - String mediaUrlsSerialized = null; - String mediaDescriptionsSerialized = null; - - if (!ListUtils.isEmpty(mediaUris)) { - List savedList = saveMedia(mediaUris, savedJsonUrls); - if (!ListUtils.isEmpty(savedList)) { - mediaUrlsSerialized = gson.toJson(savedList); - if (!ListUtils.isEmpty(savedJsonUrls)) { - deleteMedia(setDifference(savedJsonUrls, savedList)); - } - } else { - return false; - } - mediaDescriptionsSerialized = gson.toJson(mediaDescriptions); - } else if (!ListUtils.isEmpty(savedJsonUrls)) { - /* If there were URIs in the previous draft, but they've now been removed, those files - * can be deleted. */ - deleteMedia(savedJsonUrls); - } - final TootEntity toot = new TootEntity(savedTootUid, content, mediaUrlsSerialized, mediaDescriptionsSerialized, contentWarning, - inReplyToId, - replyingStatusContent, - replyingStatusAuthorUsername, - statusVisibility, - poll); - - new AsyncTask() { - @Override - protected Void doInBackground(Void... params) { - tootDao.insertOrReplace(toot); - return null; - } - }.execute(); - return true; - } - public void deleteDraft(int tootId) { TootEntity item = tootDao.find(tootId); if (item != null) { @@ -124,82 +54,4 @@ public final class SaveTootHelper { tootDao.delete(item.getUid()); } - @Nullable - private List saveMedia(@NonNull List mediaUris, - @Nullable List existingUris) { - - File directory = context.getExternalFilesDir("Tusky"); - - if (directory == null || !(directory.exists())) { - Log.e(TAG, "Error obtaining directory to save media."); - return null; - } - - ContentResolver contentResolver = context.getContentResolver(); - ArrayList filesSoFar = new ArrayList<>(); - ArrayList results = new ArrayList<>(); - for (String mediaUri : mediaUris) { - /* If the media was already saved in a previous draft, there's no need to save another - * copy, just add the existing URI to the results. */ - if (existingUris != null) { - int index = existingUris.indexOf(mediaUri); - if (index != -1) { - results.add(mediaUri); - continue; - } - } - // Otherwise, save the media. - - Uri uri = Uri.parse(mediaUri); - - String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date()); - - String mimeType = contentResolver.getType(uri); - MimeTypeMap map = MimeTypeMap.getSingleton(); - String fileExtension = map.getExtensionFromMimeType(mimeType); - String filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension); - File file = new File(directory, filename); - filesSoFar.add(file); - boolean copied = IOUtils.copyToFile(contentResolver, uri, file); - if (!copied) { - /* If any media files were created in prior iterations, delete those before - * returning. */ - for (File earlierFile : filesSoFar) { - boolean deleted = earlierFile.delete(); - if (!deleted) { - Log.i(TAG, "Could not delete the file " + earlierFile.toString()); - } - } - return null; - } - Uri resultUri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file); - results.add(resultUri.toString()); - } - return results; - } - - private void deleteMedia(List mediaUris) { - for (String uriString : mediaUris) { - Uri uri = Uri.parse(uriString); - if (context.getContentResolver().delete(uri, null, null) == 0) { - Log.e(TAG, String.format("Did not delete file %s.", uriString)); - } - } - } - - /** - * A∖B={x∈A|x∉B} - * - * @return all elements of set A that are not in set B. - */ - private static List setDifference(List a, List b) { - List c = new ArrayList<>(); - for (String s : a) { - if (!b.contains(s)) { - c.add(s); - } - } - return c; - } - -} +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt index 48466996..307fbeae 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt @@ -18,7 +18,7 @@ private const val TAG_REGEX = "(?:^|[^/)A-Za-z0-9_])#([\\w_]*[\\p{Alpha}_][\\w_] * @see * Account#MENTION_RE */ -private const val MENTION_REGEX = "(?:^|[^/[:word:]])@([a-z0-9_]+(?:@[a-z0-9\\.\\-]+[a-z0-9]+)?)" +private const val MENTION_REGEX = "(?:^|[^/[:word:]])@([a-z0-9_-]+(?:@[a-z0-9\\.\\-]+[a-z0-9]+)?)" private const val HTTP_URL_REGEX = "(?:(^|\\b)http://[^\\s]+)" private const val HTTPS_URL_REGEX = "(?:(^|\\b)https://[^\\s]+)" diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt index eaaa5e19..ce19e00e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt @@ -14,5 +14,9 @@ data class StatusDisplayOptions( @get:JvmName("cardViewMode") val cardViewMode: CardViewMode, @get:JvmName("confirmReblogs") - val confirmReblogs: Boolean + val confirmReblogs: Boolean, + @get:JvmName("hideStats") + val hideStats: Boolean, + @get:JvmName("animateEmojis") + val animateEmojis: Boolean ) \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt index 2fb9ad42..82210029 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt @@ -228,7 +228,8 @@ class StatusViewHelper(private val itemView: View) { return when (type) { Attachment.Type.IMAGE -> context.getString(R.string.status_media_images) Attachment.Type.GIFV, Attachment.Type.VIDEO -> context.getString(R.string.status_media_video) - else -> context.getString(R.string.status_media_images) + Attachment.Type.AUDIO -> context.getString(R.string.status_media_audio) + else -> context.getString(R.string.status_media_attachments) } } @@ -237,11 +238,12 @@ class StatusViewHelper(private val itemView: View) { return when (type) { Attachment.Type.IMAGE -> R.drawable.ic_photo_24dp Attachment.Type.GIFV, Attachment.Type.VIDEO -> R.drawable.ic_videocam_24dp - else -> R.drawable.ic_photo_24dp + Attachment.Type.AUDIO -> R.drawable.ic_music_box_24dp + else -> R.drawable.ic_attach_file_24dp } } - fun setupPollReadonly(poll: PollViewData?, emojis: List, useAbsoluteTime: Boolean) { + fun setupPollReadonly(poll: PollViewData?, emojis: List, statusDisplayOptions: StatusDisplayOptions) { val pollResults = listOf( itemView.findViewById(R.id.status_poll_option_result_0), itemView.findViewById(R.id.status_poll_option_result_1), @@ -259,10 +261,10 @@ class StatusViewHelper(private val itemView: View) { val timestamp = System.currentTimeMillis() - setupPollResult(poll, emojis, pollResults) + setupPollResult(poll, emojis, pollResults, statusDisplayOptions.animateEmojis) pollDescription.visibility = View.VISIBLE - pollDescription.text = getPollInfoText(timestamp, poll, pollDescription, useAbsoluteTime) + pollDescription.text = getPollInfoText(timestamp, poll, pollDescription, statusDisplayOptions.useAbsoluteTime) } } @@ -290,7 +292,7 @@ class StatusViewHelper(private val itemView: View) { } - private fun setupPollResult(poll: PollViewData, emojis: List, pollResults: List) { + private fun setupPollResult(poll: PollViewData, emojis: List, pollResults: List, animateEmojis: Boolean) { val options = poll.options for (i in 0 until Status.MAX_POLL_OPTIONS) { @@ -298,7 +300,7 @@ class StatusViewHelper(private val itemView: View) { val percent = calculatePercent(options[i].votesCount, poll.votersCount, poll.votesCount) val pollOptionText = buildDescription(options[i].title, percent, pollResults[i].context) - pollResults[i].text = pollOptionText.emojify(emojis, pollResults[i]) + pollResults[i].text = pollOptionText.emojify(emojis, pollResults[i], animateEmojis) pollResults[i].visibility = View.VISIBLE val level = percent * 100 diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java index ef1b954c..818591a1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.java @@ -20,11 +20,12 @@ import android.content.res.TypedArray; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; +import android.util.TypedValue; + import androidx.annotation.AttrRes; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatDelegate; -import android.util.TypedValue; /** * Provides runtime compatibility to obtain theme information and re-theme views, especially where diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java index 69faa6fd..ffe64a14 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java @@ -52,7 +52,7 @@ public final class ViewDataUtils { .setSensitive(visibleStatus.getSensitive()) .setIsShowingSensitiveContent(alwaysShowSensitiveMedia || !visibleStatus.getSensitive()) .setSpoilerText(visibleStatus.getSpoilerText()) - .setRebloggedByUsername(status.getReblog() == null ? null : status.getAccount().getUsername()) + .setRebloggedByUsername(status.getReblog() == null ? null : status.getAccount().getDisplayName()) .setUserFullName(visibleStatus.getAccount().getName()) .setVisibility(visibleStatus.getVisibility()) .setSenderId(visibleStatus.getAccount().getId()) @@ -60,6 +60,7 @@ public final class ViewDataUtils { .setApplication(visibleStatus.getApplication()) .setStatusEmojis(visibleStatus.getEmojis()) .setAccountEmojis(visibleStatus.getAccount().getEmojis()) + .setRebloggedByEmojis(status.getReblog() == null ? null : status.getAccount().getEmojis()) .setCollapsible(SmartLengthInputFilterKt.shouldTrimStatus(visibleStatus.getContent())) .setCollapsed(true) .setPoll(visibleStatus.getPoll()) diff --git a/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt index 44a70267..435e2450 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt @@ -4,6 +4,7 @@ package com.keylesspalace.tusky.view import android.app.Activity import android.widget.CheckBox +import android.widget.Spinner import android.widget.TextView import androidx.appcompat.app.AlertDialog import com.keylesspalace.tusky.R @@ -11,7 +12,7 @@ import com.keylesspalace.tusky.R fun showMuteAccountDialog( activity: Activity, accountUsername: String, - onOk: (notifications: Boolean) -> Unit + onOk: (notifications: Boolean, duration: Int) -> Unit ) { val view = activity.layoutInflater.inflate(R.layout.dialog_mute_account, null) (view.findViewById(R.id.warning) as TextView).text = @@ -21,7 +22,11 @@ fun showMuteAccountDialog( AlertDialog.Builder(activity) .setView(view) - .setPositiveButton(android.R.string.ok) { _, _ -> onOk(checkbox.isChecked) } + .setPositiveButton(android.R.string.ok) { _, _ -> + val spinner: Spinner = view.findViewById(R.id.duration) + val durationValues = activity.resources.getIntArray(R.array.mute_duration_values) + onOk(checkbox.isChecked, durationValues[spinner.selectedItemPosition]) + } .setNegativeButton(android.R.string.cancel, null) .show() } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java index 81e10100..c0ceeb81 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java @@ -16,10 +16,11 @@ package com.keylesspalace.tusky.viewdata; import android.os.Build; -import androidx.annotation.Nullable; import android.text.SpannableStringBuilder; import android.text.Spanned; +import androidx.annotation.Nullable; + import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Card; import com.keylesspalace.tusky.entity.Emoji; @@ -85,6 +86,7 @@ public abstract class StatusViewData { private final Status.Application application; private final List statusEmojis; private final List accountEmojis; + private final List rebloggedByAccountEmojis; @Nullable private final Card card; private final boolean isCollapsible; /** Whether the status meets the requirement to be collapse */ @@ -99,7 +101,7 @@ public abstract class StatusViewData { boolean isShowingContent, String userFullName, String nickname, String avatar, Date createdAt, int reblogsCount, int favouritesCount, @Nullable String inReplyToId, @Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled, - Status.Application application, List statusEmojis, List accountEmojis, @Nullable Card card, + Status.Application application, List statusEmojis, List accountEmojis, List rebloggedByAccountEmojis, @Nullable Card card, boolean isCollapsible, boolean isCollapsed, @Nullable PollViewData poll, boolean isBot) { this.id = id; @@ -136,6 +138,7 @@ public abstract class StatusViewData { this.application = application; this.statusEmojis = statusEmojis; this.accountEmojis = accountEmojis; + this.rebloggedByAccountEmojis = rebloggedByAccountEmojis; this.card = card; this.isCollapsible = isCollapsible; this.isCollapsed = isCollapsed; @@ -258,6 +261,10 @@ public abstract class StatusViewData { return accountEmojis; } + public List getRebloggedByAccountEmojis() { + return rebloggedByAccountEmojis; + } + @Nullable public Card getCard() { return card; @@ -324,6 +331,7 @@ public abstract class StatusViewData { Objects.equals(application, concrete.application) && Objects.equals(statusEmojis, concrete.statusEmojis) && Objects.equals(accountEmojis, concrete.accountEmojis) && + Objects.equals(rebloggedByAccountEmojis, concrete.rebloggedByAccountEmojis) && Objects.equals(card, concrete.card) && Objects.equals(poll, concrete.poll) && isCollapsed == concrete.isCollapsed; @@ -429,6 +437,7 @@ public abstract class StatusViewData { private Status.Application application; private List statusEmojis; private List accountEmojis; + private List rebloggedByAccountEmojis; private Card card; private boolean isCollapsible; /** Whether the status meets the requirement to be collapsed */ private boolean isCollapsed; /** Whether the status is shown partially or fully */ @@ -613,6 +622,11 @@ public abstract class StatusViewData { return this; } + public Builder setRebloggedByEmojis(List emojis) { + this.rebloggedByAccountEmojis = emojis; + return this; + } + public Builder setCard(Card card) { this.card = card; return this; @@ -656,7 +670,7 @@ public abstract class StatusViewData { visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount, favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, - statusEmojis, accountEmojis, card, isCollapsible, isCollapsed, poll, isBot); + statusEmojis, accountEmojis, rebloggedByAccountEmojis, card, isCollapsible, isCollapsed, poll, isBot); } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt index 593f43d3..a0f0ed68 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt @@ -119,13 +119,23 @@ class AccountViewModel @Inject constructor( } } - fun muteAccount(notifications: Boolean) { - changeRelationship(RelationShipAction.MUTE, notifications) + fun muteAccount(notifications: Boolean, duration: Int) { + changeRelationship(RelationShipAction.MUTE, notifications, duration) } fun unmuteAccount() { changeRelationship(RelationShipAction.UNMUTE) } + + fun changeSubscribingState() { + val relationship = relationshipData.value?.data + if(relationship?.notifying == true /* Mastodon 3.3.0rc1 */ + || relationship?.subscribing == true /* Pleroma */ ) { + changeRelationship(RelationShipAction.UNSUBSCRIBE) + } else { + changeRelationship(RelationShipAction.SUBSCRIBE) + } + } fun blockDomain(instance: String) { mastodonApi.blockDomain(instance).enqueue(object: Callback { @@ -177,9 +187,10 @@ class AccountViewModel @Inject constructor( /** * @param parameter showReblogs if RelationShipAction.FOLLOW, notifications if MUTE */ - private fun changeRelationship(relationshipAction: RelationShipAction, parameter: Boolean? = null) { + private fun changeRelationship(relationshipAction: RelationShipAction, parameter: Boolean? = null, duration: Int? = null) { val relation = relationshipData.value?.data val account = accountData.value?.data + val isMastodon = relationshipData.value?.data?.notifying != null if (relation != null && account != null) { // optimistically post new state for faster response @@ -197,17 +208,37 @@ class AccountViewModel @Inject constructor( RelationShipAction.UNBLOCK -> relation.copy(blocking = false) RelationShipAction.MUTE -> relation.copy(muting = true) RelationShipAction.UNMUTE -> relation.copy(muting = false) + RelationShipAction.SUBSCRIBE -> { + if(isMastodon) + relation.copy(notifying = true) + else relation.copy(subscribing = true) + } + RelationShipAction.UNSUBSCRIBE -> { + if(isMastodon) + relation.copy(notifying = false) + else relation.copy(subscribing = false) + } } relationshipData.postValue(Loading(newRelation)) } when (relationshipAction) { - RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, parameter ?: true) + RelationShipAction.FOLLOW -> mastodonApi.followAccount(accountId, showReblogs = parameter ?: true) RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId) RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId) RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId) - RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true) + RelationShipAction.MUTE -> mastodonApi.muteAccount(accountId, parameter ?: true, duration) RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId) + RelationShipAction.SUBSCRIBE -> { + if(isMastodon) + mastodonApi.followAccount(accountId, notify = true) + else mastodonApi.subscribeAccount(accountId) + } + RelationShipAction.UNSUBSCRIBE -> { + if(isMastodon) + mastodonApi.followAccount(accountId, notify = false) + else mastodonApi.unsubscribeAccount(accountId) + } }.subscribe( { relationship -> relationshipData.postValue(Success(relationship)) @@ -263,7 +294,6 @@ class AccountViewModel @Inject constructor( if (!isSelf) obtainRelationship(isReload) } - } fun setAccountInfo(accountId: String) { @@ -273,10 +303,10 @@ class AccountViewModel @Inject constructor( } enum class RelationShipAction { - FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE + FOLLOW, UNFOLLOW, BLOCK, UNBLOCK, MUTE, UNMUTE, SUBSCRIBE, UNSUBSCRIBE } companion object { const val TAG = "AccountViewModel" } -} \ No newline at end of file +} diff --git a/app/src/main/res/drawable/ic_alert_circle.xml b/app/src/main/res/drawable/ic_alert_circle.xml new file mode 100644 index 00000000..4c894f0d --- /dev/null +++ b/app/src/main/res/drawable/ic_alert_circle.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notebook.xml b/app/src/main/res/drawable/ic_notebook.xml index 2395cd14..93ff7891 100644 --- a/app/src/main/res/drawable/ic_notebook.xml +++ b/app/src/main/res/drawable/ic_notebook.xml @@ -4,5 +4,5 @@ android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"> - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notifications_active_24dp.xml b/app/src/main/res/drawable/ic_notifications_active_24dp.xml new file mode 100644 index 00000000..9a60daac --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_active_24dp.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-sw640dp/fragment_view_thread.xml b/app/src/main/res/layout-sw640dp/fragment_view_thread.xml index 52de2b95..150f0860 100644 --- a/app/src/main/res/layout-sw640dp/fragment_view_thread.xml +++ b/app/src/main/res/layout-sw640dp/fragment_view_thread.xml @@ -1,8 +1,10 @@ + android:background="?attr/windowBackgroundColor" + tools:viewBindingIgnore="true"> + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_mute_account.xml b/app/src/main/res/layout/dialog_mute_account.xml index 673fc9e4..b58a277c 100644 --- a/app/src/main/res/layout/dialog_mute_account.xml +++ b/app/src/main/res/layout/dialog_mute_account.xml @@ -22,4 +22,17 @@ app:buttonTint="@color/compound_button_color" android:text="@string/dialog_mute_hide_notifications"/> - \ No newline at end of file + + + + + diff --git a/app/src/main/res/layout/fragment_view_thread.xml b/app/src/main/res/layout/fragment_view_thread.xml index 433f1ed0..806d420a 100644 --- a/app/src/main/res/layout/fragment_view_thread.xml +++ b/app/src/main/res/layout/fragment_view_thread.xml @@ -1,9 +1,11 @@ + android:layout_gravity="top" + tools:viewBindingIgnore="true"> - - - - - - - - - - - - - - - - - - - - - - - - - - - + diff --git a/app/src/main/res/layout/item_draft.xml b/app/src/main/res/layout/item_draft.xml new file mode 100644 index 00000000..5708e08f --- /dev/null +++ b/app/src/main/res/layout/item_draft.xml @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_media_preview.xml b/app/src/main/res/layout/item_media_preview.xml new file mode 100644 index 00000000..27b58e7a --- /dev/null +++ b/app/src/main/res/layout/item_media_preview.xml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index 9abeaf86..5c2dcb42 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -257,192 +257,8 @@ app:layout_constraintStart_toStartOf="@id/status_display_name" app:layout_constraintTop_toBottomOf="@id/status_card_view" tools:visibility="visible"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/app/src/main/res/layout/item_status_detailed.xml b/app/src/main/res/layout/item_status_detailed.xml index 95f82a4a..f8be1c61 100644 --- a/app/src/main/res/layout/item_status_detailed.xml +++ b/app/src/main/res/layout/item_status_detailed.xml @@ -206,189 +206,7 @@ android:importantForAccessibility="noHideDescendants" app:layout_constraintTop_toBottomOf="@id/status_card_view"> - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -588,4 +406,4 @@ app:layout_constraintTop_toTopOf="@id/status_reply" app:srcCompat="@drawable/ic_more_horiz_24dp" /> - \ No newline at end of file + diff --git a/app/src/main/res/layout/item_status_notification.xml b/app/src/main/res/layout/item_status_notification.xml index ecf84ab5..228cac57 100644 --- a/app/src/main/res/layout/item_status_notification.xml +++ b/app/src/main/res/layout/item_status_notification.xml @@ -145,8 +145,6 @@ android:layout_marginRight="14dp" android:layout_marginBottom="14dp" android:contentDescription="@string/action_view_profile" - android:paddingRight="12dp" - android:paddingBottom="12dp" android:scaleType="centerCrop" tools:ignore="RtlHardcoded,RtlSymmetry" tools:src="@drawable/avatar_default" /> diff --git a/app/src/main/res/layout/toolbar_basic.xml b/app/src/main/res/layout/toolbar_basic.xml index 71039105..47bd2d90 100644 --- a/app/src/main/res/layout/toolbar_basic.xml +++ b/app/src/main/res/layout/toolbar_basic.xml @@ -1,19 +1,16 @@ - + - + android:layout_height="?attr/actionBarSize" /> - - - - - \ No newline at end of file + diff --git a/app/src/main/res/menu/account_toolbar.xml b/app/src/main/res/menu/account_toolbar.xml index ee881122..d25bcdc1 100644 --- a/app/src/main/res/menu/account_toolbar.xml +++ b/app/src/main/res/menu/account_toolbar.xml @@ -2,18 +2,10 @@ - - - - diff --git a/app/src/main/res/menu/drafts.xml b/app/src/main/res/menu/drafts.xml new file mode 100644 index 00000000..bbc9202f --- /dev/null +++ b/app/src/main/res/menu/drafts.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 8c1a0305..8348000b 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -36,7 +36,7 @@ الحسابات المحظورة طلبات المتابعة عدل ملفك التعريفي - المسودات + المسودات الرّخص \@%s شارَكَه %s @@ -441,13 +441,13 @@ إضافة استطلاع رأي افتح دائما التبويقات التي تحتوي على محتوى حساس استطلاع رأي - 5 دقائق - 30 دقيقة - ساعة واحدة - 6 ساعات - يوم واحد - 3 أيام - 7 أيام + 5 دقائق + 30 دقيقة + ساعة واحدة + 6 ساعات + يوم واحد + 3 أيام + 7 أيام ضف خيارا خيارات متعددة الخيار %d diff --git a/app/src/main/res/values-ber/strings.xml b/app/src/main/res/values-ber/strings.xml index 2e0412ca..9726d4ff 100644 --- a/app/src/main/res/values-ber/strings.xml +++ b/app/src/main/res/values-ber/strings.xml @@ -16,7 +16,7 @@ ⵏⴰⴸⵉ ⵣⵔⴻⴳ ⴰⵎⴰⵖⵏⵓ ⴼⴼⴻⵖ - ⵉⵔⴻⵡⵡⴰⵢⴻⵏ + ⵉⵔⴻⵡⵡⴰⵢⴻⵏ ⵉⵙⵎⴻⵏⵢⵉⴼⴻⵏ ⵓⵖⴰⵍ ⴽⴻⵎⵎⴻⵍ diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml new file mode 100644 index 00000000..61c97505 --- /dev/null +++ b/app/src/main/res/values-bg/strings.xml @@ -0,0 +1,506 @@ + + + Публикация + Публикацията, на която сте изготвили отговор, е премахната + 1 час + 30 минути + 5 минути + Неопределено + Продължителност + Анкета + Активиране на плъзгащия жест за превключване между раздели + Показване на филтър за известия + Търсенето бе неуспешно + Акаунти + Акаунтът е от друг сървър. Да изпратите ли и там анонимно копие на доклада\? + Докладът ще бъде изпратен на модератора на вашия сървър. Можете да предоставите обяснение защо докладвате този акаунт по-долу: + Извличането на състояния бе неуспешно + Докладването бе неуспешно + Препращане към %s + Допълнителни коментари + Успешно докладване на @%s + Готово + Назад + Продължаване + + Остава %d секунда + Остават %d секунди + + + Остава %d минута + Остават %d минути + + + Остава %d час + Остават %d часа + + + Остава %d ден + Остават %d дни + + Анкета, която създадохте, приключи + Анкета, в която сте гласували, приключи + Гласуване + затворено + завършва в %s + + %s човек + %s човека + + + %s глас + %s гласа + + " <!-- 15 votes • 1 hour left --> %1$s • %2$s" + %1$s • %2$s + Действия за изображение %s + Сигурни ли сте, че искате да изчистите окончателно всичките си известия\? + Композиране + Композиране на публикация + Прилагане + Филтриране + Изчистване + Списък + Избиране на списък + Хаштагове + Хаштаг без # + Добавяне на хаштаг + Име на списък + Анкета с избори: %1$s, %2$s, %3$s, %4$s; %5$s + Директно + Последователи + Публично + Отметнато + Поставено в любими + Реблог + Без описание + Предупреждение за съдържание: %s + Мултимедия: %s + достигнати са максималните %1$d раздела + %1$s, %2$s и %3$d други + %1$s + %1$s и %2$s + Поставено в любими от + Споделено от + + <b>%s</b> Споделяне + <b>%s</b> Споделяния + + + <b>%1$s</b> Любимо + <b>%1$s</b> Любими + + Закачане + Разкачане + Информацията по-долу може да отразява непълно потребителския профил. Натиснете, за да отворите пълен профил в браузъра. + Използване на абсолютно време + Съдържание + Етикет + добавяне на данни + Профилни метаданни + CC-BY-SA 4.0 + CC-BY 4.0 + Лицензиран под лиценза Apache (копие по-долу) + Tusky съдържа код и активи от следните проекти с отворен код: + Отсподеляне + Споделяне с оригиналната аудитория + %1$s се премести в: + Бот + Изтеглянето се провали + Текущият набор от емоджита на Google + Първо ще трябва да изтеглите тези емоджи комплекти + Стандартният емоджи комплект на Mastodon + Blob емоджитата, известни от Android 4.4–7.1 + Емоджи комплектът по подразбиране в устройство ви + Рестартиране + По-късно + Ще трябва да рестартирате Tusky, за да приложите тези промени + Изисква се рестартиране на приложението + Отваряне на публикация + Разгъване/свиване на всички състояния + Извършва се търсене… + По подразбиране от системата + Стил на емоджи + Копирано в клипборда + Инстанцията ви %s няма персонализирани емоджита + Композиране + Копие от публикацията е запазено във вашите чернови + Изпращането е отменено + Изпращане на публикации + Грешка при изпращане на публикация + Изпращане на публикация… + Запазване на чернова\? + Изисква ръчно одобряване на последователи + Заключване на акаунт + Премахване + Задаване на надпис + Опишете за хора със зрителни увреждания +\n(%d ограничение на знаците) + Неуспешно задаване на надпис + Публикуване с акаунт %1$s + Премахване на акаунт от списъка + Добавяне на акаунт към списъка + Търсене на хора, които следвате + Редакция на списъка + Изтриване на списъка + Преименуване на списъка + Създаване на списък + Списъкът не можа да се изтрие + Списъкът не можа да се създаде + Списъкът не можа да се преименува + Списъчна емисия + Списъци + Списъци + Добавяне на нов Mastodon акаунт + Добавяне на акаунт + Фраза за филтриране + Когато ключовата дума или фраза е само буквено-цифрова, тя ще бъде приложена само ако съответства на цялата дума + Цяла дума + Актуализиране + Премахване + Редакция на филтър + Добавяне на филтър + Разговори + Публични емисии + зареждане на още + Отговаряне на @%s + Мултимедия + Винаги разгъване на публикации, маркирани с предупреждения за съдържание + Винаги показване на деликатно съдържание + Следва ви + %dс + %dм + %dч + %dд + %dг + след %dс + след %dм + след %dч + след %dг + след %dд + Заявено последване + Прикачени файлове + Аудио + Видео + Изображения + Споделяне на връзка към публикация + Споделяне на съдържание на публикация + Профилът на Tusky + Доклади за грешки и заявки за функции: +\n https://github.com/tuskyapp/Tusky/issues + Уебсайт на проекта: +\n https://tusky.app + Tusky е свободен софтуер с отворен код. Той е лицензиран под Общият публичен лиценз на GNU Версия 3. Можете да видите лиценза тук: https://www.gnu.org/licenses/gpl-3.0.en.html + Осъществено от Tusky + Tusky %s + Относно + Заключен акаунт + %d нови взаимодействия + %1$s и %2$s + %1$s, %2$s, и %3$s + %1$s, %2$s, %3$s и %4$d други + %s ви спомена + Известия, когато някой, за когото сте абонирани, публикува + Нови публикации + Известия за приключили анкети + Анкети + Известия, когато публикациите ви бъдат означени като любими + Любими + Известия, когато публикациите ви се споделят + Най-малък + Скрито + Раздели + Филтриране на емисия + Анимиране на персонализирани емоджита + Показване на цветни градиенти за скрита мултимедия + Анимиране на GIF аватари + Показване на индикатор за ботове + Език + Скриване на бутона за композиране, при превъртане + Използване на персонализирани раздели чрез Chrome + Браузър + Използване на системния дизайн + Автоматично при залез + Черно + Светло + Тъмно + Филтри + Емисии + Тема на приложение + Външен вид + някой, за когото съм абониран, публикува + приключили анкети + публикациите ми са сложени в любими + публикациите ми са споделени + заявка за последване + последвани + споменати + Уведомете ме когато + Уведомяване със светлина + Уведомяване с вибрация + Уведомяване със звук + Сигнали + Известия + Известия + Директно: Публикуване само за споменатите потребители + Само за последователи: Публикуване само за последователи + Публично: Публикуване в публични емисии + Скриване на известия + Заглушаване на @%s\? + Блокиране на @%s\? + Скриване на целия домейн + Сигурни ли сте, че искате да блокирате всички от %s\? Няма да виждате съдържание от този домейн в нито една публична емисия или във вашите известия. Последователите ви от този домейн ще бъдат премахнати. + Изтриване и преработване на тази публикация\? + Изтриване на тази публикация\? + Отследване на този акаунт\? + Отмяна на заявката за последване\? + Изтегляне + Качване… + Завършване на мултимедийно качване + "Тук може да се въведе адресът или домейнът на която и да е инстанция, като mastodon.social, icosahedron.website, social.tchncs.de и <a href=\"https://instances.social\">други!</a> +\n +\nАко все още нямате акаунт, можете да въведете името на инстанцията, към който искате да се присъедините, и да създадете акаунт там. +\n +\nИнстанцията е единично място, където се хоства акаунтът ви, но можете лесно да комуникирате и да следвате хора в други инстанции, сякаш сте на същия сайт. +\n +\nПовече информация можете да намерите на <a href=\"https://joinmastodon.org\">joinmastodon.org</a>. "more! + \n\nIf you don\'t yet have an account, you can enter the name of the instance you\'d like to + join and create an account there.\n\nAn instance is a single place where your account is + hosted, but you can easily communicate with and follow folks on other instances as though + you were on the same site. + \n\nMore info can be found at joinmastodon.org. + + Свързване… + Какво е инстанция\? + Заглавна част + Аватар + Отговор… + Няма резултати + Търсене… + Био + Показвано име + Предупреждение за съдържание + Какво се случва\? + Коя инстанция\? + Отговорът е изпратен успешно. + Изпратено! + %s е разкрит + Потребителят е раззаглушен + Потребителят е деблокиран + Изпратено! + Споделяне на мултимедия в… + Споделяне на публикация в… + Споделяне на URL адреса на публикацията в… + Теглене на мултимедия + Изтегляне на мултимедия + Споделяне като … + Отваряне като %s + Копиране на връзката + Изтегляне на %1$s + Отваряне на мултимедия #%d + Връзки + Споменавания + Хаштагове + Показване на любими + Показване на споделяния + Отваряне на споделилия автор + Хаштагове + Споменавания + Връзки + Добавяне на раздел + Нулиране + Планиране на публикация + Емоджи клавиатура + Предупреждение за съдържание + Видимост на публикация + Планирани публикации + Чернови + Търсене + Отхвърляне + Приемане + Отмяна + Редакция + Редакция на профил + Запазване + Отваряне на чекмедже + Скриване на мултимедия + Споменаване + Раззаглушаване на разговор + Заглушаване на разговор + Раззаглушаване на %s + Заглушаване на %s + Заглушаване на известия от %s + Раззаглушаване на известия от %s + Раззаглушаване на %s + Раззаглушаване + Заглушаване + Споделяне + Снимане + Добавяне на анкета + Добавяне на мултимедия + Отваряне в браузър + Мултимедия + Заявки за последване + Скрити домейни + Блокирани потребители + Заглушени потребители + Отметки + Любими + Предпочитания за акаунт + Предпочитания + Профил + Затваряне + Повторен опит + ПУБЛИКУВАНЕ! + ИЗПРАЩАНЕ + Изтриване и преработване + Изтриване + Редакция + Докладване + Показване на споделяния + Скриване на споделяния + Деблокиране + Блокиране + Отследване + Последване + Сигурни ли сте, че искате да излезете от акаунта %1$s\? + Излизане + Влизане с Mastodon + Композиране + Още + Премахване от любими + Отмятане + Поставяне в любими + Премахване на споделяне + Споделяне + Отговор + Бърз отговор + Допълнителни коментари\? + Докладване на @%s + %s току-що публикува + %s поиска да ви последва + %s ви последва + %s постави вашата публикация в любими + %s сподели вашата публикация + Нищо тук. Дръпнете надолу, за да опресните! + Нищо тук. + Свиване + Разгъване + Покажи по-малко + Покажи повече + Щракнете за преглед + Мултимедията е скрита + Деликатно съдържание + %s сподели + \@%s + Лицензи + Оповестявания + Планирани публикации + Чернови + Редакция на профила ви + Заявки за последване + Скрити домейни + Блокирани потребители + Заглушени потребители + Отметки + Любими + Последователи + Последвани + Закачени + С отговори + Публикации + Раздели + Директни съобщения + Локално + Известия + Начало + Грешка при изпращане на публикация. + Качването бе неуспешно. + Изображения и видеоклипове не могат да бъдат прикачени към едно и също състояние. + Изисква се разрешение за съхранение на мултимедия. + Изисква се разрешение за четене на носител. + Този файл не можа да бъде отворен. + Този тип файл не може да бъде качен. + Аудио файловете трябва да са по-малки от 40MB. + Видео файловете трябва да са по-малки от 40MB. + Файлът трябва да е по-малък от 8MB. + Състоянието е твърде дълго! + Получаването на токен за вход бе неуспешно. + Упълномощаването е отказано. + Възникна неидентифицирана грешка при упълномощаване. + Неуспешно намиране на уеб браузър, който да се използва. + Неуспешно удостоверяване с тази инстанция. + Въведен е невалиден домейн + Това не може да бъде празно. + Възникна грешка в мрежата! Моля, проверете връзката си и опитайте отново! + Възникна грешка. + Черновата е изтрита + Неуспешно зареждане на информация за отговор + Стари чернови + Функцията за чернови в Tusky е напълно преработена, за да бъде по-бърза, по-лесна за ползване и по-малко бъгава. +\n Все още можете да осъществите достъп до старите си чернови чрез бутон на екрана за нови чернови, но те ще бъдат премахнати при бъдеща актуализация! + Тази публикация не успя да се изпрати! + Наистина ли искате да изтриете списъка %s\? + Не можете да качите повече от %1$d мултимедийни прикачени файлове. + Скриване на количествена статистика на профили + Скриване на количествена статистика на публикации + Ограничаване на известия от емисия + Преглед на известията + Част от информацията, която може да повлияе на вашето психично състояние, ще бъде скрита. Това включва: +\n +\n - Известия за Любими/Споделяния/Последвани +\n - Брой Любими/Споделяния на публикации +\n - Статистика за Последователи/Публикации на профили +\n +\n Изскачащите известия няма да бъдат засегнати, но можете да прегледате предпочитанията си за известяване ръчно. + Запазено! + Вашата лична бележка за този акаунт + Благосъстояние + Скриване на заглавието на горната лента с инструменти + Показване на диалоговия прозорец за потвърждение преди споделяне + Показване на визуализации на връзки в емисии + Mastodon има минимален интервал за планиране от 5 минути. + Няма оповестявания. + Нямате планирани състояния. + Нямате чернови. + Грешка при търсенето на публикация %s + Редакция + Избор %d + Множество избора + Добавяне на избор + 7 дни + 3 дни + 1 ден + 6 часа + Споделяния + Известия за заявки за последване + Заявки за последване + Известия за нови последователи + Нови последователи + Известия за нови споменавания + Нови споменавания + Най-голям + Голям + Среден + Малък + Скрито: Не се показва в публични емисии + Размер на текста на състоянието + Само за последователи + Скрито + Публично + Долу + Горе + Основна навигационна позиция + Синхронизирането на настройките бе неуспешно + Публикуване (синхронизирано със сървър) + Винаги маркиране на мултимедия като чувствителна + Поверителност на публикация по подразбиране + HTTP прокси порт + HTTP прокси сървър + Активиране на HTTP прокси + HTTP прокси + Прокси + Изтегляне на визуализации за мултимедии + Показване на отговори + Показване на споделяния + \ No newline at end of file diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 2584ce38..a2a4ca26 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -292,7 +292,7 @@ মিডিয়া লুকানো সংবেদনশীল কন্টেন্ট লাইসেন্সগুলি - খসড়াগুলো + খসড়াগুলো আপনার প্রোফাইল সম্পাদনা করুন অনুরোধ অনুসরণ করুন অবরুদ্ধ ব্যবহারকারী @@ -345,19 +345,19 @@ পছন্দ %d একাধিক পছন্দ পছন্দ যুক্ত করুন - ৭ দিন - ৩ দিন - ১ দিন - ৬ ঘন্টা - ১ ঘন্টা - ৩০ মিনিট - ৫ মিনিট + ৭ দিন + ৩ দিন + ১ দিন + ৬ ঘন্টা + ১ ঘন্টা + ৩০ মিনিট + ৫ মিনিট ভোটগ্রহণ সরান পোল যুক্ত করুন সর্বদা সামগ্রী সতর্কতা সহ চিহ্নিত টুটগুলি প্রসারিত করুন অনুসন্ধান করতে ব্যর্থ - অক্কোউন্টগুলি + অ্যাকাউন্টগুলো যখন শব্দ বা বাক্যাংশটি শুধুমাত্র আলফানিউমেরিক হয় তখন এটি শুধুমাত্র তখনই প্রয়োগ করা হবে যদি এটি সম্পূর্ণ শব্দটির সাথে মেলে সম্পূর্ণ শব্দ বিজ্ঞপ্তি ফিল্টার দেখান @@ -424,4 +424,15 @@ এই জায়গা খালি হতে পারে না। একটি নেটওয়ার্ক ত্রুটি ঘটেছে! আপনার সংযোগ পরীক্ষা করে আবার চেষ্টা করুন! একটি ত্রুটি ঘটেছে। + + %1$sটি পছন্দ + %1$sটি পছন্দ + + %s দৃশ্যমান + %s পোস্ট করেছে + %s তোমাকে ফলো করতে চায় + %s তোমাকে ফলো করেছে + %s তোমার টুট বুস্ট করেছে + %s তোমার টুট বুস্ট করেছে + ঘোষণা \ No newline at end of file diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index 33899e5b..4fab27c6 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -36,7 +36,7 @@ অবরুদ্ধ ব্যবহারকারী অনুরোধ অনুসরণ করুন আপনার প্রোফাইল সম্পাদনা করুন - খসড়াগুলো + খসড়াগুলো লাইসেন্সগুলি \@%s %s সমর্থন দিয়েছে @@ -395,13 +395,13 @@ অনুসন্ধান করতে ব্যর্থ পোল যুক্ত করুন ভোটগ্রহণ - ৫ মিনিট - ৩০ মিনিট - ১ ঘন্টা - ৬ ঘন্টা - ১ দিন - ৩ দিন - ৭ দিন + ৫ মিনিট + ৩০ মিনিট + ১ ঘন্টা + ৬ ঘন্টা + ১ দিন + ৩ দিন + ৭ দিন পছন্দ যুক্ত করুন একাধিক পছন্দ পছন্দ %d diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index eac118b4..c0fcd2bd 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -2,20 +2,20 @@ S\'ha produït un error. Això no pot estar buit. - El domini introduït no és vàlid - L\'autenticació en aquesta instància ha fallat. + El domini que s\'ha introduït no és vàlid + Ha fallat l\'autenticació en aquesta instància. No s\'ha trobat cap navegador web per a utilitzar. S\'ha produït un error d\'autorització no identificat. S\'ha denegat l\'autorització. - L\'obtenció del token d\'inici de sessió ha fallat. + Ha fallat l\'obtenció del token d\'inici de sessió. L\'estat és massa llarg! - El fitxer ha de ser inferior a 8MB. - Aquest tipus de fitxer no es pot pujar. - Aquest tipus de fitxer no es pot obrir. - Cal permís d\'accés al emmagatzematge. - Cal permís d\'escriptura en el dispositiu. + El fitxer ha de ser d\'una mida menor de 8MB. + No es pot pujar aquest tipus de fitxer. + No es pot obrir aquest tipus de fitxer. + Cal permís d\'accés a l\'emmagatzematge. + Cal permís d\'escriptura a l\'emmagatzematge. No es poden adjuntar imatges i vídeos en el mateix estat. - La pujada ha fallat. + Ha fallat la pujada. Inici Notificacions Local @@ -29,7 +29,7 @@ Usuaris blocats Peticions de seguiment Edita el perfil - Esborranys + Esborranys \@%s %s tootejat Contingut sensible @@ -107,7 +107,7 @@ , però pots comunicar-te fàcilment i seguir amics d\'altres instàncies com si fossiu en el mateix lloc. \n\nTens més informació a joinmastodon.org. - S\'està finalitzant la pujada de materila multimèdia + S\'està finalitzant la pujada de material multimèdia S\'està pujant… Baixa Vols deixar de seguir aquest compte? @@ -191,7 +191,7 @@ En resposta a @%s carrega\'n més Vota - S\'ha produït un error en enviar el toot. + S\'ha produït un error en enviar el tut. Pestanyes Llicències Amplia @@ -202,15 +202,15 @@ Missatges directes No hi ha res aquí. Elimina l\'impuls - S\'ha produït un error de connexió! Comprova la connexió i torna-ho a provar! - Els fitxers de vídeo han de pesar menys de 40 MB. + S\'ha produït un error de connexió! Comproveu la connexió i torneu-ho a provar! + Els fitxers de vídeo han de ser de mida menor de 40 MB. Multimèdia amagada Amaga Estàs segur de tancar la sessió de %1$s\? Amaga els retoots Mostra els impulsos Elimina i reecririu - Open drawer + Obre el menú Visibilitat del toot Contingut sensible Afegir una pestanya @@ -225,7 +225,7 @@ Baixa el fitxer Compartir la imatge a … Enviat! - Follow requested + S\'ha enviat la petició de seguiment Amb respostes Teclat d\'emojis Obrir el media #%d @@ -334,8 +334,8 @@ %1$s Favorits - - + %s impuls + %s impulsos Impulsat per Marcat favorit per @@ -361,7 +361,7 @@ Vols netejar totes les notificacions permanentment\? %1$s • %2$s - %s vots + %s vot %s vots Acaba a %s @@ -399,16 +399,16 @@ \@%s reportat satisfactoriament El compte és d\'un altre servidor. Enviar, igualment, una copia anònima del report\? Cerca fallida - 1 hora - 6 hores + 1 hora + 6 hores Edita Afegeix una enquesta Enquesta - 5 minuts - 30 minuts - 1 dia - 3 dies - 7 dies + 5 minuts + 30 minuts + 1 dia + 3 dies + 7 dies Afegeix una tria Múltiples tries Tria %d @@ -421,13 +421,13 @@ Programar el toot Reiniciar Desenvolupat per Tusky - Afegit a les adreces d\'interès. + S\'ha afegit a les adreces d\'interès Seleccionar la llista Llista S\'ha produït un error en cercar la publicació %s No tens cap estat planificat. - Els fitxers d\'àudio han de ser més petits que 40MB. - No tens cap esborrany + Els fitxers d\'àudio han de ser de mida menor de 40MB. + No teniu cap esborrany. L\'interval mínim de planificació a Mastodon és de 5 minuts. Peticions de seguiment Mostra el diàleg de confirmació abans de promoure @@ -455,4 +455,46 @@ Desactivar les notificacions per %s Activar les notificacions per %s Deixar de silenciar %s + Revisió d\'avisos + S\'ha desat! + Les vostres notes quant a aquest compte + Benestar + Amaga el títol de la barra d\'eines superior + No hi ha cap avís. + Indefinit + Durada + + falta %d segon + falten %d segons + + + falta %d minut + falten %d minuts + + + falta %d hora + falten %d hores + + + falta %d dia + falten %d dies + + Adjuncions + Àudio + Notificacions quan algú a qui esteu subscrit publica un tut nou + Tuts nous + emojis personalitzats animats + algú a qui estic subscrit acaba de publicar un tut nou + %s acaba de fer una publicació + Avisos + S\'ha esborrat el tut del qual en vau fer un esborrany de resposta + S\'ha eliminat l\'esborrany + No s\'ha pogut carregar la informació de la resposta + Esborranys antics + No s\'ha pogut enviar aquest tut! + Segur que voleu esborrar la llista %s\? + No podeu pujar més de %1$d adjunts multimèdia. + Amaga les estadístiques quantitatives dels perfils + Amaga les estadístiques quantitatives de les publicacions + Limita les notificacions de la cronologia \ No newline at end of file diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml new file mode 100644 index 00000000..56f4f5e9 --- /dev/null +++ b/app/src/main/res/values-ckb/strings.xml @@ -0,0 +1,479 @@ + + + چی خەریکه ڕوودەدات؟ + کام نموونە؟ + وەڵام دانەوە کە بە سەرکەوتوویی نێردرا. + ناردن! + %s نەشاراوە + بەکارهێنەر نەگۆڕاو + بەکارهێنەر بەربەست نەکراوە + ناردن! + هاوبەشکردنی میدیا بۆ… + هاوبەشی کردن بە توت بۆ… + هاوبەشکردنی توتی URL بۆ… + داگرتنی میدیا + داگرتنی میدیا + هاوبەش کردن وەک … + کردنەوە وەک %s + بەستەرەکە ڕوونوس بکە + داگرتنی %1$s + کردنەوەی میدیا #%d + بەستەرەکان + ئاماژەکان + هاشتاگی + پیشاندانی دڵخوازەکان + پیشاندانی بەهێزکردنەکان + کردنەوەی بەهێزکردنی نووسەر + هاشتاگ + ئاماژەکان + بەستەرەکان + زیادکردنی سەرخشت + ڕیسێت کردن + خشتەی توت + تەختەکلیلی ئیمۆجی + ئاگاداری ناوەڕۆک + بینینی توت + توتی خشتەکراو + ڕەشنووسەکان + گەڕان + ڕەتکردنەوە + ڕازیبون + گەڕانەوە + بژارکردن + دەستکاری پرۆفایل بکە + بپارێزە + کردنەوەی وێنەکێش + شاردنەوەی میدیا + ئاماژە + گفتوگۆی لاببە + نابێدەنگ کردن %s + بێدەنگکردن %s + بێدەنگکردنی ئاگانامەکان لە %s + ئاگانامەکانی لاببە لە %s + نابێدەنگ %s + بێدەنگی لابردن + بێدەنگ + هاوبەش کردن + وێنە بگرە + زیادکردنی ڕاپرسی + زیادکردنی میدیا + کردنەوە لە وێبگەڕ + میدیا + بەدواداچونی داواکاریەکان بکە + دۆمەینە شاراوەکان + بەکارهێنەرە بلۆککراوەکان + بەکارهێنەرە گۆڕاوەکان + نیشانەکان + دڵخوازەکان + پەسەندکراوەکانی ئەژمێر + پەسەندەکان + پرۆفایل + دابخە + دووبارە هەوڵ بدە + توت! + توت + سڕینەوە و دووبارە-ڕەشنووس + سڕینەوە + دەستکاری + گوزارشەکان + پیشاندانی بەهێزکردنەکان + شاردنەوەی بەهێزکردنەکان + بەربەست کردن لاببە + بلۆک + بەدوادانەچو + بەدواداکەوتن + ئایا دڵنیایت لەوەی دەتەوێت بچیتەدەرەوە لە هەژماری %1$s؟ + چوونەدەرەوە + چوونەژوورەوە لەگەڵ ماستۆدۆن + دروستکردن + زیاتر + لابردنی دڵخوازەکان + نیشانه + دڵخواز + لابردنی بەهێزکردن + بەهێزکردن + وەڵام + وەڵامدانەوەی خێرا + سەرنجەکانی تر؟ + گوزارشت @%s + %s تەنها بڵاوکرایەوە + %s داواکراوە کە شوێنت بکەوێت + %s بەدواتا کەوت + %s خۆشترین توتەکەت + %s توتەکەتی بەرزکردەوە + هیچ شتێک لێرە نیە ڕاکە خوارەوە بۆ نوێکردنەوە! + هیچ شتێک لێرە نیە. + نوشتانەوە + فراوانکردن + کەمتر نیشان بدە + زیاتر پیشان بدە + کرتە بکە بۆ بینین + میدیا شاراوە + ناوەڕۆکی هەستیار + %s بەرزکرا + \@%s + مۆڵەتەکان + ڕاگه یه نراوەکان + توتی خشتەکراو + دەستکاری پرۆفایلەکەت بکە + بەدواداچونی داواکاریەکان بکە + دۆمەینە شاراوەکان + بەکارهێنەرە بلۆککراوەکان + بەکارهێنەرە بێدەنگ + نیشانەکان + دڵخوازەکان + شوێنکەوتوان + بەدوادا + چەسپا + لەگەڵ وەڵامەکان + بابەتەکان + توت + سەرخشتەکان + نامە ڕاستەوخۆکان + گشتی + ناوخۆیی + ئاگادارییەکان + سەرەتا + هەڵە لە ناردنی توت. + بارکردن سەرکەوتوو نەبوو. + وێنە و ڤیدیۆکان ناتوانرێت هەردووک هاوپێچ بکرێت لەگەڵ یەک دۆخ. + مۆڵەت بۆ پاشکەوتکردنی میدیا پێویستە. + مۆڵەت بۆ خوێندنەوەی میدیا پێویستە. + ئەم فایلە ناتوانرێت بکرێتەوە. + ناتوانرێت ئەو جۆرە فایلە باربکرێت. + فایلەکانی دەنگ دەبێت کەمتر بێت لە ٤٠MB. + پێویستە فایلەکانی ڤیدیۆ کەمتر لە 40 مێگابایت بن. + فایلەکە دەبێت کەمتر بێت لە 8 مێگابایت. + ڕەستە زۆر درێژە! + سەرکەوتوو نەبوو لە بەدەستهێنانی نیشانەی چوونەژوورەوە. + ڕێپێدان ڕەتکرایەوە. + هەڵەیەک بۆ مۆڵەتدانی نەناسراو ڕووی دا. + نەیتوانی وێبگەڕبدۆزێتەوە بۆ بەکارهێنان. + سەرکەوتوو نەبوو، ڕاستکردنەوە لەگەڵ ئەم نمونەیە. + دۆمەینی نادروست تێنووسکرا + ئەمە ناتوانێت بەتاڵ بێت. + هەڵەیەک لە تۆڕ ڕوویدا! تکایە پەیوەندیت بپشکنە و دوبارە هەوڵ بدە! + هەڵەیەک ڕوویدا. + تایبەتمەندی بابەت گریمانەیی + دەرگای پرۆکسی HTTP + ڕاژەکاری پرۆکسی HTTP + چالاککردنی پرۆکسی HTTP + HTTP proxy + پرۆکسی + داگرتنی پێشبینینی میدیا + وەڵامدانەوەکان پیشان بدە + پیشاندانی بەهێزکردنەکان + سەرخشتەکان + فلتەرکردنی تایملاین + نمرەی لاری ڕەنگاوڕەنگ نیشان بدە بۆ میدیای شاراوە + وێنۆجکەی ئەنیمەی GIF + نیشاندەر نیشاندەر بۆ بۆتەکان نیشان بدە + زمان + دوگمەی ئاوازدانان بشارەوە لەکاتی خشاندن + بەکارهێنانی خشتەبەندەکانی دڵخواز + وێبگەڕ + دیزاینی سیستەم بەکاربهێنە + خۆکار لە کاتی خۆرئاوابووندا + ڕەش + ڕووناکی + تاریک + فلتەرەکان + ڕووکاری ئەپ + تایملاین + دەرکەوتن + کەسێک کە من بەشدارم لە بڵاو کردنەوەی توتێکی نوێیرکری + ڕاپرسی کۆتایی هاتووە + بابەتەکانی من پەسەندن + پۆستەکانم بەرزدەکرانەوه + بەدواداچوونەوەی داواکراو + بەدوادا + ناوبراو + ئاگادارم بکەوە کاتێک + ئاگاداربکەوە بە ڕووناکی + ئاگادارکردنەوەی لەلەرینە + ئاگادارکردنەوەی بە دەنگێک + ئاگادارکردنەوەکان + ئاگانامەکان + ئاگانامەکان + ڕاستەوخۆ: تەنها بۆ بەکارهێنەرانی ناوبراو پۆست بکە + تەنها شوێنکەوتوانی: تەنها پۆست بۆ شوێنکەوتوانی + لیستی نەکراو: لە هێڵی کاتی گشتی دا پیشان مەدە + گشتی: پۆست بکە بۆ هێڵی کاتی گشتی + شاردنەوەی ئاگانامەکان + بێدەنگکردن @%s؟ + بلۆککردنی @%s؟ + شاردنەوەی هەموو دۆمەینەکە + ئایا دڵنیایت لەوەی دەتەوێت هەموو %s بلۆک بکەیت؟ تۆ ناوەڕۆکێک نابینیت لە دۆمەینەکە لە هیچ هێڵی کاتی گشتی یان لە ئاگانامەکانت. شوێنکەوتوانی تۆ لەو دۆمەینەوە لادەبرێن. + ئەم دووانە بسڕەوە و دووبارە ڕەشنووس یان دەکەیتەوە؟ + ئەم توتە بسڕەوە؟ + شوێن نەکەوتنی ئەم هەژمارە؟ + داواکاری بەدوادا چوەکان هەڵوەشانەوە؟ + داگرتن + بارکردن… + تەواوکردنی بارکردنی میدیا + ناونیشان یان دۆمەینی هەر نمونەیەک دەکرێت لێرە تێبنووسرێت، وەک فرەتر! +\n +\nئەگەر هێشتا ئەژمێرێکت نیە، دەتوانیت ناوی ئەو نمونەیە داخڵ بکەیت کە دەتەوێت بیبەستیت و ئەژمێرێک دروست بکەیت لەوێ. +\n +\nنموونەیەک تاکە شوێنە کە ئەژمێرەکەت میوانداری کراوە، بەڵام دەتوانیت بە ئاسانی پەیوەندی لەگەڵ بکەیت و دوای ئەو خەڵکانە بکەویت لە نمونەکانی تر وەک ئەوەی تۆ لە هەمان سایت دابیت. +\n +\nزانیاری زیاتر دەتوانرێت بدۆزرێتەوە لە joinmastodon.org. + گرێدان… + نموونەیەک چییە؟ + سەرپەڕە + وێنۆچکە + وەڵام… + هیچ ئەنجامێک نیە + گەڕان… + دەربارە + ناوی پیشاندان + ئاگاداری ناوەڕۆک + گفتوگۆی بێدەنگ + + %d کاژێرماوە + %d کاژێرماوە + + کاتێک وشەکە یان دەستەواژەکە تەنها ئەبجەدییە، تەنها ئەگەر لەگەڵ هەموو وشەکە یەکبێت کاری پێدەکرێت + ناتوانیت زیاتر لە %1$d هاوپێچی میدیا باربکەیت. + شاردنەوەی زانیاری چەندێتی لەسەر پرۆفایلەکان + شاردنەوەی زانیاری چەندێتی لە بابەتەکان + سنووردارکردنی ئاگانامەکانی تایم لاین + پێداچوونەوەی ئاگانامەکان + هەندێک زانیاری کە لەوانەیە کاریگەری لەسەر باشبوونی دەروونیت دروست بکات دەشاردرێنەوە. ئەمە پێکدێت لە: +\n +\n- ئاگانامەکانی پەسەند/بەهێزکردن/بەدوادا +\n - پەسەندترین/بەرزکردنەوە لەسەر توت +\n - بەدواداچوون/زانیاری بابەت لەسەر پرۆفایلەکان +\n +\nکارتێکردنی ئاگانامەکانی پاڵپێوەنان، بەڵام دەتوانیت بە پەسەندکردنە ئاگانامەکانت دا بخشێنیەوە بە دەستی. + ڕزگارکرا + تێبینی تایبەتی تۆ دەربارەی ئەم ئەژمێرە + Wellbeing + شاردنەوەی ناونیشانی شریتی ئامڕازی سەرەوە + پیشاندانی دیالۆگی دووپاتکردنەوە پێش بەهێزکردن + نیشاندانی پێشاندانی بەستەر لە هێڵی کات + ماستۆدۆن کەمترین ماوەی خشتەی هەیە لە ٥ خولەک. + هیچ ڕاگه یه نراوێک له بەرده رنه کەون. + هیچ بارێکی خشتەکراوت نیە. + هیچ ڕەشنووسێکت نییە. + هەڵە لە گەڕان بەدوای بابەت %s + دەستکاریکردن + هەڵبژاردنی %d + چەند هەڵبژاردنێک + زیادکردنی هەڵبژاردن + ڕاپرسی + چالاککردنی ئاماژەکردنی لێدانی چالاک بۆ گۆڕین لە نێوان خشتەبەندەکان + تاسکی کۆد و سەرمایەکانی تێدایە لەم پڕۆژە کراوەی سەرچاوە: + فلتەری ئاگانامەکان نیشان بدە + گەڕانەکە سەرکەوتوو نەبوو + ئەژمێرەکان + هەژمارەلە ڕاژەیەکی دیکەیە ترە. کۆپیەکی بێ سەروبەر بنێرە بۆ ڕاپۆرتەکە لەوێ؟ + ڕاپۆرتەکە دەنێردرێت بۆ بەڕێوەبەری ڕاژەکەت. دەتوانیت ڕوونکردنەوەیەک پێشکەش بکەیت کە بۆچی ئەم ئەژمێرە لە خوارەوە ڕاپۆرت دەکەیت: + سەرکەوتوو نەبوو لە هێنانی بارەکان + ڕاپۆرتکردن سەرکەوتوو نەبوو + ناردنەوە بۆ %s + سەرنجەکانی زیاتر + سەرکەوتووانە ڕاپۆرتکرا @%s + تەواوبوو + دواوە + بەردەوام بە + + %d چرکەی ماوەو + %d دووەم چەپ + + + %d خولەک ماوە + %d خولەک ماوە + + + %d ڕۆژ ماوە + %d ڕۆژ ماوە + + ڕاپرسییەک کە دروستت کردووە کۆتایی هات + ڕاپرسییەک کە دەنگی پێداویت کۆتایی هات + دەنگ + داخراوە + کۆتایی دێت لە %s + + %s کەس + %s کەس + + + %s دەنگ + %s دەنگ + + %1$s • %2$s + کارەکان بۆ وێنە %s + ئایا دڵنیایت لەوەی دەتەوێت بە هەمیشەیی هەموو ئاگانامەکانت بسڕیتەوە؟ + دروستکردن + دروستکردنی توت + جێبەجێ کردن + فلتەر + سڕینەوە + لیست + دیاریکردنی لیست + هاشتاگی + هاشتاگی بێ # + هاشتاگی زیاد بکە + ناوی لیست + ڕاپرسی لەگەڵ هەڵبژاردنەکان: %1$s, %2$s, %3$s, %4$s; %5$s + ڕاستەوخۆ + شوێنکەوتوانی + لە لیست نەکراو + گشتی + نیشانکراوە + پەسەندکراو + دووبارە بڵاگ کرا + هیچ وەسفێک + ئاگاداری ناوەڕۆک: %s + میدیا: %s + بەرزترین رێژەی خشتەبەندەکانی %1$d گەیشت + %1$s, %2$s و %3$d زیاتر + %1$s و %2$s + %1$s + پەسەندکراوە لەلایەن + بەرزکراوە لەلایەن + + %s بەهێزکردن + %s بەهێزکردن + + + %1$s دڵخواز + %1$s دڵخواز + + Pin + لابردن + ڕەنگە زانیاری خوارەوە ڕەنگدانەوەی پرۆفایلی بەکارهێنەر بە ناتەواوی بێت. فشار بکە بۆ کردنەوەی پرۆفایلی تەواو لە وێبگەڕەکە. + کاتی ڕەها بەکاربهێنە + ناوەڕۆک + ناونیشان + داتا زیاد بکە + مێتاداتای پرۆفایل + CC-BY-SA 4.0 + CC-BY 4.0 + مۆڵەتدراوە لەژێر مۆڵەتی ئەپاچی (لەبەرگیراوە لە خوارەوە) + بێ هێزکردن + بەرزکردنەوە بۆ جەماوەری ڕەسەن + %1$s گواسترایەوە بۆ: + بۆت + داگرتن سەرکەوتوو نەبوو + کۆمەڵە ئیمۆجیەکەی ئێستای گووگڵ + سێتی ئیمۆجی پێوانەیی ماتۆدۆن + ئیمۆجی Blob لە ئەندرۆید ەوە ناسراوە 4.4–7.1 + سێتی ئیمۆجی بنەڕەتی ئامێرەکەت + دەستپێکردنەوە + دواتر + تۆ پێویستە توسکی دەستپێبکەیتەوە بۆ ئەوەی ئەم گۆڕانکاریانە جێبەجێ بکەیت + دەسپێکردنەوەی کاربەرنامە پێویستە + کردنەوە توت + فراوانکردن/نوشتانەوەی هەموو بارەکان + ئەنجامدانی گەڕان… + تۆ پێویستە سەرەتا ئەم سێتە ئیمۆجییانە دابگریت + سیستەمی بنەڕەت + شێوازی ئیمۆجی + ڕوونووسکراوە بۆ کلیپ بۆرد + نموونەکەت %s هیچ ئیمۆجییەکی ئاسایی نییە + دروستکردن + کۆپیەکی دەستنووسەکە خەزن کراوە بۆ ڕەشنووسەکانت + ناردنی هەڵوەشاوە + ناردنی توتس + هەڵە لە ناردنی توت + (توت) دەنێرم… + ڕەشنووس پاشەکەوت بکەیت؟ + داوات لێدەکات کە بە دەستی شوێنکەوتوانی پەسەند بکە + داخستنی ئەژمێر + لابردن + دانانی سەردێڕ + وەسف بکە بۆ بینایی داڕماو +\n(%d سنوری کاراکتەر) + دانانی سەردێڕ شکستی هێنا + بڵاوکردنەوە بە هەژماری %1$s + لابردنی ئەژمێر لە لیستەکە + زیادکردنی ئەژمێر بۆ لیستەکە + گەڕان بەدوای ئەو کەسانەی کە پەیڕەوی ان دەکەیت + دەستکاریکردنی لیستەکە + سڕینەوەی لیستەکە + ناونانەوەی لیستەکە + دروستکردنی لیستێک + نەیتوانی لیستەکە بسڕێتەوە + نەیتوانی ناوی لیست بنووسرێ + نەیتوانی لیست دروست بکات + لیستی تایم لاین + لیستەکان + لیستەکان + زیادکردنی ئەژمێری ماتۆدۆنی نوێ + زیادکردنی ئەژمێر + دەستەواژە بۆ فلتەر + هەموو وشەکە + نوێکردنەوە + لابردن + دەستکاریکردنی فلتەر + زیادکردنی فلتەر + گفتوگۆکان + هێڵی کاتی گشتی + بارکردنی زیاتر + وەڵام دانەوە بۆ @%s + میدیا + هەمیشە ئەو توتانەی کە بە ئاگادارکردنەوەکانی ناوەڕۆکەوە نیشانەکراون فراوان بکە + هەمیشە ناوەڕۆکی هەستیار نیشان بدە + دوای تۆ دەکەوێت + %ds + %dm + %dh + %dd + %dy + لە %ds + لە %dm + لە %dh + لە %dd + لە %dy + بەدواداچوونەوەی داواکراو + ڤیدیۆ + وێنەکان + هاوبەشکردنی لینک بۆ توت + هاوبەشکردنی ناوەڕۆکی دووت + پرۆفایلی تاسکی + ڕاپۆرتەکانی هەڵەکان و داواکاریەکانی تایبەتمەندی: +\nhttps://github.com/tuskyapp/Tusky/issues + وێبسایتی پڕۆژە: +\nhttps://tusky.app + توسکی سۆفتوێری ئازاد و سەرچاوەی کراوەیە مۆڵەتدراوە بە پێ نامەی گشتی GNU Public Version 3. دەتوانیت لێرە مۆڵەتەکە نیشان بدەی: https://www.gnu.org/licenses/gpl-3.0.en.html + لەلایەن تاسکیەوە دەست کراوە بە + توسکی %s + سەبارەت + هەژماری داخراو + %d چالاکی نوێ + %1$s و %2$s + %1$s و %2$s و %3$s + %1$s, %2$s, %3$s و %4$d ئەوانی تر + %s ئاماژەی بە تۆ کرد + ئاگانامەکان کاتێک کەسێک کە تۆ بەشداریت کردووە لە بڵاوکردنەوەی توتێکی نوێ + توتی نوێ + ئاگادارییەکان دەربارەی ڕاپرسییەکان کە کۆتایی هاتووە + ڕاپرسییەکان + ئاگانامەکان کاتێک کەتوتەکان نیشانە کراون وەک دڵخواز + دڵخوازەکان + ئاگانامەکان کاتێک کە دووتەکەت بەرز دەکرێتەوە + بەهێزکردن + ئاگانامەکان دەربارەی داواکاریەکانی بەدوادا + بەدواداچونی داواکاریەکان بکە + ئاگانامەکان دەربارەی شوێنکەوتوانی نوێ + شوێنکەوتوانی نوێ + ئاگانامەکان دەربارەی ئاماژە نوێیەکان + ئاماژە نوێیەکان + گەورەترین + گەورە + مامناوەندی + بچووک + بچووکترین + قەبارەی دەقی بار + شوێنکەوتوانی تەنها + لە لیست نەکراو + گشتی + خوارەوە + سەرەوە + شوێنی سەرەکی ڕێنیشاندەر + سەرکەوتوو نەبوو لە هاودەمکردنی ڕێکبەندەکان + بڵاوکردنەوە (هاوکاتکراوە لەگەڵ سێرڤەر) + هەمیشە میدیا وەک هەستیار نیشان بکە + \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 63eac75b..68c7188e 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -36,7 +36,7 @@ Blokovaní uživatelé Žádosti o sledování Upravit váš profil - Koncepty + Koncepty Licence \@%s %s boostnul/a @@ -422,13 +422,13 @@ Tento účet je z jiného serveru. Chcete na něj také poslat anonymizovanou kopii\? Zobrazit filtr oznámení Anketa - 5 minut - 30 minut - 1 hodinu - 6 hodin - 1 den - 3 dny - 7 dní + 5 minut + 30 minut + 1 hodinu + 6 hodin + 1 den + 3 dny + 7 dní Přidat možnost Lze zvolit více možností Možnost %d diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index e14d4b58..ad210b07 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -32,7 +32,7 @@ Defnyddwyr wedi\'u blocio Dilyn ceisiadau Golygu\'ch Proffil - Drafftiau + Drafftiau Trwyddedau %s wedi\'u hybu Cynnwys sensitif diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 2fbd5c25..4ea468a9 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -36,7 +36,7 @@ Blockierte Profile Folgeanfragen Dein Profil bearbeiten - Entwürfe + Entwürfe Lizenzen \@%s %s teilte @@ -395,13 +395,13 @@ Dieses Konto ist von einem anderen Server. Soll eine anonymisierte Kopie des Berichts auch dorthin geschickt werden\? Benachrichtigungsfilter anzeigen Umfrage - 5 Minuten - 30 Minuten - 1 Stunde - 6 Stunden - 1 Tag - 3 Tage - 7 Tage + 5 Minuten + 30 Minuten + 1 Stunde + 6 Stunden + 1 Tag + 3 Tage + 7 Tage Editieren test %s Umfrage hinzufügen @@ -470,4 +470,22 @@ Titel der Hauptnavigation verstecken Im Moment gibt es keine Ankündigungen. Ankündigungen + Der Beitrag auf den du antworten willst wurde gelöscht + Entwurf gelöscht + Alte Entwürfe + Das \"Entwürfe\"-Feature in Tusky wurde komplett neu gestaltet um schneller und benutzerfreundlicher zu sein. +\nDu kannst deine alten Entwürfe noch hinter einem Button bei den neuen Entwürfen finden, aber sie werden mit einem zukünftigen Update gelöscht! + Dieser Beitrag konnte nicht gesendet werden! + Willst du die Liste %s wirklich löschen\? + Du kannst nicht mehr als %1$d Anhänge hochladen. + Wohlbefinden + Dauer + Für immer + Anhänge + Audio + Benachrichtigungen, wenn jemand, den ich abonniert habe, etwas Neues veröffentlicht + Neue Beiträge + GIF-Emojis animieren + Jemand, den ich abonniert habe, etwas Neues veröffentlicht + %s hat gerade etwas gepostet \ No newline at end of file diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 4731a229..bfe88277 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -36,7 +36,7 @@ Blokitaj uzantoj Petoj de sekvado Redakti vian profilon - Malnetoj + Malnetoj Permesiloj \@%s %s diskonigis @@ -407,13 +407,13 @@ Aldoni baloton Ĉiam pligrandigi tootoj markiĝita per enhavaj avertoj Baloto - 5 minutoj - 30 minutoj - 1 horo - 6 horoj - 1 tago - 3 tagoj - 7 tagoj + 5 minutoj + 30 minutoj + 1 horo + 6 horoj + 1 tago + 3 tagoj + 7 tagoj Aldoni elekton Multaj elektoj Elekton %d diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index a334c567..93e90774 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -36,7 +36,7 @@ Bloqueados Solicitudes Editar tu perfil - Borradores + Borradores Licencias \@%s %s compartió @@ -420,13 +420,13 @@ Error al buscar Añadir encuesta Encuesta - 5 minutos - 30 minutos - 1 hora - 6 horas - 1 día - 3 días - 7 días + 5 minutos + 30 minutos + 1 hora + 6 horas + 1 día + 3 días + 7 días Añadir opción Opciones múltiples Opción %d @@ -479,4 +479,33 @@ Tu nota privada acerca de esta cuenta No hay anuncios. Anuncios + %s recién publicado + No puedes cargar más de %1$d archivos adjuntos multimedia. + Esconder las estadísticas cuantitativas de los perfiles + Esconder las estadísticas cuantitativas de las publicaciones + Revisar Notificaciones + Bienestar + Notificaciones cuando alguien al que estoy suscrito publicó un nuevo toot + Nuevos toots + alguien al que estoy suscrito publicó un nuevo toot + Algunas informaciones que podríam afectar tu bienestar van a ser ocultas. Esto incluye: +\n +\n- Notificaciones de favoritos, impulsos e seguidores +\n- Conteo de favoritos e impulsos en toots +\n- Estadísticas de seguidores e toots en perfiles +\n +\nLas notificaciones Push no serán afectadas, pero puedes revisar manualmente tus preferencias. + El toot al que redactaste una respuesta ha sido eliminado + Borrador eliminado + Error al cargar la información de respuesta + Borradores antiguos + La función de borrador en Tusky se ha rediseñado por completo para que sea más rápida, más fácil de usar y con menos errores. +\nAún puede acceder a sus borradores antiguos a través de un botón en la pantalla de borradores nuevos, ¡pero se eliminarán en una actualización futura! + ¡Este toot no se pudo enviar! + ¿Realmente quieres eliminar la lista %s\? + Indefinido + Duración + Adjuntos + Audio + Limitar notificaciones de cronología \ No newline at end of file diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 19d3a3a8..8498bba0 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -32,7 +32,7 @@ Blokeatuak Eskakizunak Profila editatu - Zirriborroak + Zirriborroak Lizentziak %s-(e)k bultzatu du Kontuz edukiarekin @@ -417,13 +417,13 @@ Bilaketa huts egin du Erakutsi jakinarazpenen iragazkia Inkesta - 5 minutu - 30 minutu - Ordu 1 - 6 ordu - Egun 1 - 3 egun - 7 egun + 5 minutu + 30 minutu + Ordu 1 + 6 ordu + Egun 1 + 3 egun + 7 egun Gehitu aukera Aukera anitzak %d. aukera diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 970f4acc..68fa3bc5 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -32,7 +32,7 @@ کاربران مسدود درخواست‌های پی‌گیری ویرایش نمایه‌تان - پیش‌نویس‌ها + پیش‌نویس‌ها پروانه‌ها %s تقویت کرد محتوای حسّاس @@ -400,13 +400,13 @@ شکست در جست‌وجو نمایش پالایهٔ آگاهی‌ها نظرسنجی - ۵ دقیقه - ۳۰ دقیقه - ۱ ساعت - ۶ ساعت - ۱ روز - ۳ روز - ۷ روز + ۵ دقیقه + ۳۰ دقیقه + ۱ ساعت + ۶ ساعت + ۱ روز + ۳ روز + ۷ روز افزودن گزینه گزینه‌های چندگانه گزینهٔ %d diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index cc20833d..a677b67e 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -36,7 +36,7 @@ Comptes bloqués Demandes d’abonnement Modifier votre profil - Brouillons + Brouillons Licences \@%s %s a partagé @@ -94,7 +94,7 @@ Mentionner Cacher les médias Ouvrir le menu - Sauvegarder + Enregistrer Modifier le profil Modifier Annuler @@ -425,13 +425,13 @@ Toujours ouvrir les pouets avec un contenu sensible Ajouter un sondage Sondage - 5 minutes - 30 minutes - 1 heure - 6 heures - 1 jour - 3 jours - 7 jours + 5 minutes + 30 minutes + 1 heure + 6 heures + 1 jour + 3 jours + 7 jours Ajouter un choix Choix multiples Choix %d @@ -488,4 +488,21 @@ Votre note privée sur ce compte Il n’y a pas d’annonce. Annonces + Certaines informations susceptibles d\'affecter votre bien-être mental seront cachées. Il s\'agit : +\n +\n - des notifications de favoris, de partage et de suivi +\n - du compte des favoris/partages sur les pouets +\n - des statistiques sur les profils +\n +\n Les notifications \"push\" ne seront pas affectées, mais vous pouvez revoir vos préférences de notification manuellement. + une personne à laquelle je suis abonné a publié un nouveau pouet + %s vient de publier + Examiner les notifications + Nouveau pouets + Vous ne pouvez pas téléverser plus de %1$d pièces jointes. + Bien-être + Notifications quand quelqu\'un que vous suivez publie un nouveau pouet + Limiter les notifications de la timeline + Cacher les statistiques quantitatives sur les profils + Cacher les statistiques quantitatives sur les publications \ No newline at end of file diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index af024759..2ba114b5 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -177,7 +177,7 @@ Roghanna Cuntais Sainroghanna Logáil Amach - Dréachtaí + Dréachtaí Roghaí Theip ar fhíordheimhniú leis an gcás sin. Cad is sampla ann\? @@ -445,13 +445,13 @@ Taispeáin scagaire Fógraí Cumasaigh gotha swipe aistriú idir cluaisíní Vótaíocht - 5 nóiméad - 30 nóiméad - 1 uair an chloig - 6 uair an chloig - 1 lá - 3 lá - 7 lá + 5 nóiméad + 30 nóiméad + 1 uair an chloig + 6 uair an chloig + 1 lá + 3 lá + 7 lá Cuir rogha leis Ilroghanna Rogha %d diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index ed594dc7..83bfce33 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -8,7 +8,7 @@ Roighainnean cunntais Roighainnean Clàraich a-mach - Dreachd + Dreachd Prìomhaich Dè a th ’ann an àite\? Deasaich diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index f46d69a4..4591a85e 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -2,9 +2,9 @@ हिंदी पसंदीदा - प्रारूप + प्रारूप लॉग आउट - पसंद + प्राथमिकताएं खाता प्राथमिकताएं प्रोफाइल एडिट करें खोज @@ -200,13 +200,13 @@ विकल्प जोड़ें विकल्प %d कई विकल्प - 7 दिन - 3 दिन - 1 दिन - 6 घंटे - 1 घंटा - 30 मिनिट - 5 मिनट + 7 दिन + 3 दिन + 1 दिन + 6 घंटे + 1 घंटा + 30 मिनिट + 5 मिनट %d घंटा शेष %d घंटे शेष @@ -403,4 +403,6 @@ %d सेकेंड शेष %d सेकेंड शेष + पोस्ट बहुत लंबा है! + उस सर्वर से प्रमाणित करने में विफल। \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index bd211848..38dd5130 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -33,10 +33,10 @@ Követő Kedvencek Némított felhasználók - Blokkolt felhasználók + Letiltott felhasználók Követési kérelmek Profilod szerkesztése - Piszkozatok + Piszkozatok Licenszek \@%s %s megtolta @@ -65,8 +65,8 @@ Biztosan ki szeretnél jelentkezni a következőből: %1$s? Követés Követés vége - Blokkolás - Blokkolás feloldása + Letiltás + Letiltás feloldása Megtolások elrejtése Megtolások mutatása Bejelentés @@ -80,7 +80,7 @@ Fiókbeállítások Kedvencek Némított felhasználók - Blokkolt felhasználók + Letiltott felhasználók Követési kérelmek Média Megnyitás böngészőben @@ -118,7 +118,7 @@ Tülk URL megosztása… Tülk megosztása… Elküldve! - Felhasználó blokkolása feloldva + Felhasználó letiltása feloldva Felhasználó némítása feloldva Elküldve! Válasz sikeresen elküldve. @@ -237,12 +237,12 @@ Listák Törlés Fiók zárolása - Elmented a vázlatot? + Elmented a piszkozatot\? Tülk elküldése… A tülk elküldése nem sikerült Tülkök elküldése Küldés megszakítva - A tülk másolatát elmentettük a vázlataid közé + A tülk másolatát elmentettük a piszkozataid közé Szerkesztés A %s szervernek nincsenek egyedi emoji-jai Vágólapra másolva @@ -417,13 +417,13 @@ Sikertelen keresés Szavazás hozzáadása Szavazás - 5 perc - 30 perc - 1 óra - 6 óra - 1 nap - 3 nap - 7 nap + 5 perc + 30 perc + 1 óra + 6 óra + 1 nap + 3 nap + 7 nap Válasz hozzáadása Több lehetőség Válasz %d @@ -442,7 +442,7 @@ Lista kiválasztása Lista A hangfájloknak kisebbnek kell lenniük, mint 40 MB. - Nincs egy vázlatod sem. + Nincs egy piszkozatod sem. Nincs egy ütemezett tülköd sem. A Mastodonban a legrövidebb ütemezhető időintervallum 5 perc. Követési kérelmek @@ -462,7 +462,7 @@ Színes homály mutatása rejtett médiánál követni szeretnének Értesítések elrejtése - Blokkolod: @%s\? + Letiltod: @%s\? Elnémítsuk @%s fiókot\? Beszélgetés némításának feloldása Beszélgetés némítása @@ -476,4 +476,34 @@ Saját, mások számára nem látható megjegyzés erről a fiókról Nincsenek közlemények. Közlemények + A Tülköt, melyre válaszul piszkozatot készítettél törölték + Piszkozat törölve + Nem sikerült a Válasz információit betölteni + Régi Piszkozatok + A Tusky piszkozat funkcióját teljesen újraterveztük, hogy gyorsabb, felhasználóbarátabb és hibamentesebb legyen. +\nTovábbra is elérheted a régi piszkozataidat egy gombbal az új piszkozatok képernyőjén, de ezeket egy későbbi frissítésben el fogjuk törölni! + Ez a tülk nem küldődött el! + Tényleg le akarod törölni a %s listát\? + Nem tölthetsz fel %1$d médiacsatolmányból többet. + Profilok mérőszámainak elrejtése + Tülkök mérőszámainak elrejtése + Idővonali értesítések korlátozása + Értesítések Áttekintése + Pár információ, ami befolyásolhatja a mentális egészségedet rejtve marad. Ilyenek pl.: +\n +\n - Kedvenc/Megtolás/Bekövetés értesítései +\n - Kedvenc/Megtolás számlálók a tülkökön +\n - Követő/Tülk statisztikák a profilokon +\n +\nA Push-értesítéseket ez nem befolyásolja, de kézzel átállíthatod az értesítési beállításaidat. + Végtelen + Időtartam + Csatolmányok + Audio + Értesítések általam követett személy új tülkjeiről + Új tülkök + valaki, akit követek újat tülkölt + %s épp tülkölt + Jóllét + Egyedi emojik animálása \ No newline at end of file diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 974dfc5f..6f8fe739 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -3,7 +3,7 @@ Skrá inn með Mastodon Hvað er tilvik\? Eftirlæti - Drög + Drög Skrá út Kjörstillingar Eiginleikar tengingar @@ -396,13 +396,13 @@ Tókst ekki að leita Birta tilkynningasíu Athuga - 5 mínútur - 30 mínútur - 1 klukkustund - 6 klukkustundir - 1 dagur - 3 dagar - 7 dagar + 5 mínútur + 30 mínútur + 1 klukkustund + 6 klukkustundir + 1 dagur + 3 dagar + 7 dagar Bæta við valkosti Margir valkostir Valkostur %d diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 436dd419..dbaae3f2 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -36,7 +36,7 @@ Utenti bloccati Richieste di seguirti Modifica il tuo profilo - Bozze + Bozze Licenze \@%s %s ha boostato @@ -431,13 +431,13 @@ Errore durante la ricerca Mostra il filtro delle notifiche Sondaggio - 5 minuti - 30 minuti - 1 ora - 6 ore - 1 giorno - 3 giorni - 7 giorni + 5 minuti + 30 minuti + 1 ora + 6 ore + 1 giorno + 3 giorni + 7 giorni Aggiungi scelta Scelte multiple Scelta %d @@ -468,7 +468,7 @@ La tua nota privata su questo account Nascondi il titolo della barra degli strumenti in alto Mostra la finestra di dialogo di conferma prima del boosting - Mostra le anteprime dei collegamenti nelle sequenze temporali + Mostra le anteprime dei collegamenti nelle timelines Mastodon ha un intervallo minimo di programmazione di 5 minuti. Non ci sono annunci. Non hai stati pianificati. @@ -483,4 +483,14 @@ Riattiva le notifiche da %s Annunci Richieste di seguirti + Nascondi statistiche quantitative sui profili + Nascondi le statistiche quantitative sui post + Limita le notifiche della timeline + Revisiona le notifiche + Benessere + Notifiche di quando qualcuno a cui sei iscritto ha pubblicato un nuovo toot + Nuovi toots + qualcuno a cui sono iscritto ha pubblicato un nuovo toot + %s appena pubblicato + Non puoi caricare più di %1$d allegati multimediali. \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index cd170803..e019ff61 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -35,7 +35,7 @@ ブロックしたユーザー フォローリクエスト プロフィールを編集 - 下書き + 下書き ライセンス %sさんがブーストしました 閲覧注意 @@ -370,13 +370,13 @@ 参加した投票の結果がでました 作成した投票の結果がでました 投票 - 5分 - 30分 - 1時間 - 6時間 - 1日 - 3日 - 7日 + 5分 + 30分 + 1時間 + 6時間 + 1日 + 3日 + 7日 選択肢を追加 複数選択可 編集 diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 30217b62..ece80a67 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -2,7 +2,7 @@ Qqen ɣer Maṣṭudun Ismenyifen - Irewwayen + Irewwayen Ffeɣ Iɣewwaṛen Iɣewwaṛen n umiḍan @@ -196,13 +196,13 @@ Tella-d tuccḍa deg cetki Tucḍa n unadi Assenqed - 5 n tisdidin - 30 n tisdidin - 1 n usrag - 6 n isragen - 1 n wass - 3 n wussan - 7 n wussan + 5 n tisdidin + 30 n tisdidin + 1 n usrag + 6 n isragen + 1 n wass + 3 n wussan + 7 n wussan Tafrant %d Ig ṭṭafar Imeḍfaṛen diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 0f430f76..454f3208 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -37,7 +37,7 @@ 숨긴 도메인 팔로우 요청 프로필 편집 - 임시 저장 + 임시 저장 라이선스 \@%s %s님이 부스트 했습니다 @@ -409,13 +409,13 @@ 열람주의로 설정된 툿을 항상 펼치기 투표 추가 투표 - 5분 - 30분 - 1시간 - 6시간 - 1일 - 3일 - 7일 + 5분 + 30분 + 1시간 + 6시간 + 1일 + 3일 + 7일 항목 추가 여러 항목 선택 가능 %d번 항목 diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 72119eef..4f4a7868 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -3,7 +3,7 @@ മസ്റ്റഡോൺ വഴി പ്രവേശിക്കുക എന്താണ് ഒരു ഇൻസ്റ്റൻസ്\? പ്രിയപ്പെട്ടവ - കരടുകൾ + കരടുകൾ പുറത്തിറങ്ങുക മുൻഗണനകൾ അക്കൗണ്ട് മുൻഗണനകൾ @@ -111,4 +111,5 @@ അറിയിപ്പുകൾ ടാബുകൾ അറിയിപ്പുകൾ + പ്രഖ്യാപനങ്ങൾ \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 77413bb6..85e1b2ea 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -36,7 +36,7 @@ Geblokkeerde gebruikers Volgverzoeken Profiel bewerken - Concepten + Concepten Licenties \@%s %s boostte @@ -418,13 +418,13 @@ Meldingenfilter tonen Heel woord Wanneer het trefwoord of zinsdeel alfanumeriek is, wordt het alleen gefilterd wanneer het hele woord overeenkomt - 5 minuten - 30 minuten - 1 uur - 6 uur - 1 dag - 3 dagen - 7 dagen + 5 minuten + 30 minuten + 1 uur + 6 uur + 1 dag + 3 dagen + 7 dagen Voeg keuze toe Meerdere keuzes Keuze %d diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index b47a8c80..36e2fc9c 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -36,7 +36,7 @@ Blokkerte brukere Forespørsler om følgen Endre profilen din - Kladder + Kladder Lisenser \@%s %s boosted @@ -408,13 +408,13 @@ Ekspander alltid toots markert med innholdsadvarsel Legg til avstemming Avstemming - 5 minutter - 30 minutter - 1 time - 6 timer - 1 dag - 3 dager - 7 dager + 5 minutter + 30 minutter + 1 time + 6 timer + 1 dag + 3 dager + 7 dager Legg til valg Flere valg Valg %d @@ -467,4 +467,36 @@ Ditt private notat om denne kontoen Det er ingen kunngjøringer. Kunngjøringer + Skjul kvantitativ informasjon på profiler + Skjul kvantitativ informasjon på toots + Begrens tidslinjevarsler + Se over varsler + Informasjon som kan påvirke ditt mentale velvære vil bli skjult. Dette inkluderer: +\n +\n - Varsler om favorisering, boosts og følgere +\n - Antall favoriseringer og boots på toots +\n - Antall følgere og toots på profiler +\n +\n Push-varsler vil ikke påvirkes, men du kan se over dine varselinnstillinger manuelt. + Velvære + Varsler når noen jeg følger publiserer en ny toot + Nye toots + noen jeg følger publiserer en ny toot + %s tootet akkurat + Du kan ikke laste opp flere enn %1$d mediavedlegg. + Uendelig + Varighet + Er du sikker på at du vil slette listen %s\? + Vedlegg + Lyd + Tootet du kladdet et svar til har blitt fjernet + Kladd slettet + Lasting av svarinformasjon feilet + Gamle kladder + KladdfunksjonaLiteten i Tusky er skrevet om og er nå kjappere, mer brukervennlig, og med færre feil. +\nGamle kladder er fortsatt tilgjengelige via en knapp på den nye kladdskjermen, men de vil bli fjernet i en fremtidig oppdatering! + Sending av toot feilet! + Animer egendefinerte emojis + Avslutt abonnementet + Abonner \ No newline at end of file diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index c493fd0a..a35c08ab 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -31,7 +31,7 @@ Utilizaires blocats Demandas d’abonament Modificar lo perfil - Borrolhons + Borrolhons Licéncias %s partejat Contengut sensible @@ -419,13 +419,13 @@ Fracàs de la recèrca Ajustar un sondatge Sondatge - 5 minutas - 30 minutas - 1 ora - 6 oras - 1 jorn - 3 jorns - 7 jorns + 5 minutas + 30 minutas + 1 ora + 6 oras + 1 jorn + 3 jorns + 7 jorns Ajustar d’opcions Opcions multiplas Opcion %d diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/app/src/main/res/values-pa/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index cd9dfd69..1ea10dc1 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -31,7 +31,7 @@ Zablokowani użytkownicy Prośby o możliwość śledzenia Edytuj profil - Szkice + Szkice Licencje %s podbił Wrażliwe treści @@ -430,13 +430,13 @@ Wyszukiwanie nie powidło się Pokaż filtr powiadomień Głosowanie - 5 minut - 30 minut - 1 godzina - 6 godzin - 1 dzień - 3 dni - 7 dni + 5 minut + 30 minut + 1 godzina + 6 godzin + 1 dzień + 3 dni + 7 dni Dodaj wybór Kilka wyborów Opcja %d diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 83c57c00..bcf47ec4 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -34,7 +34,7 @@ Usuários bloqueados Seguidores pendentes Editar perfil - Rascunhos + Rascunhos Licenças %s deu boost Mídia sensível @@ -236,7 +236,7 @@ %dh %dm %ds - Te segue + te segue Sempre mostrar mídia sensível Mídia Respondendo @%s @@ -417,13 +417,13 @@ Contas Erro ao pesquisar Enquete - 5 minutos - 30 minutos - 1 hora - 6 horas - 1 dia - 3 dias - 7 dias + 5 minutos + 30 minutos + 1 hora + 6 horas + 1 dia + 3 dias + 7 dias Adicionar opção Múltiplas opções Opção %d @@ -472,4 +472,36 @@ Silencie notificações de %s Dessilencie notificações de %s Ocultar o título da barra superior de tarefas + Notificar sobre toots de quem me interessa + quem me interessa tootar + Erro ao carregar toot para responder + Erro ao enviar o toot! + O toot em que se rascunhou uma resposta foi excluído + Rascunho excluído + A função de rascunhos no Tusky foi totalmente redesenhada para ser mais rápida, mais fácil e com menos erros. +\nÉ possível acessar rascunhos antigos através de um botão na tela de novos rascunhos, mas serão removidos numa futura atualização! + Rascunhos antigos + Não é possível anexar mais de %1$d arquivos de mídia. + Ocultar status dos perfis + Ocultar status dos toots + Limitar notificações da linha do tempo + Revisar notificações + Algumas informações que podem afetar seu bem-estar serão ocultadas. Isso inclui: +\n +\n- Notificações de favoritos, boosts e seguidores +\n- Número de favoritos e boosts nos toots +\n- Status de toots e seguidores nos perfis +\n +\nNotificações push não serão afetadas, mas é possível revisar sua preferência manualmente. + Salvo! + Nota pessoal sobre esta conta aqui + Bem-estar + Sem comunicados. + Indefinido + Duração + Anexos + Áudio + Novos toots + %s recém tootou + Comunicados \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index aa65f29f..6724cafe 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -36,7 +36,7 @@ Список блокировки Запросы на подписку Редактировать профиль - Черновики + Черновики Лицензии \@%s %s продвинул(а) @@ -444,13 +444,13 @@ Аккаунты Поиск завершился ошибкой Опрос - 5 минут - 30 минут - 1 час - 6 часов - 1 день - 3 дня - 7 дней + 5 минут + 30 минут + 1 час + 6 часов + 1 день + 3 дня + 7 дней Добавить Множественный выбор Вариант %d @@ -505,4 +505,16 @@ Скрыть заголовок в верхней панели Объявлений нет. Объявления + "Некоторая информация, которая может повлиять на ваше психическое благополучие, будет скрыта. Это включает в себя: +\n +\n - Избранное/Продвижение/Уведомления подписок +\n - Избранное/Продвижение счета на тутах +\n - Статистика подписчиков/публикаций в профилях +\n +\n На push-уведомления это не повлияет, но вы можете просмотреть настройки уведомлений вручную." + Благосостояние + Неопределённая + Продолжительность + Вложения + Аудио \ No newline at end of file diff --git a/app/src/main/res/values-sa/strings.xml b/app/src/main/res/values-sa/strings.xml index f466ecc8..fd79b509 100644 --- a/app/src/main/res/values-sa/strings.xml +++ b/app/src/main/res/values-sa/strings.xml @@ -36,7 +36,7 @@ \@%s अनुज्ञापत्राणि कालबद्धदौत्यानि - लेखविकर्षाः + लेखविकर्षाः स्वीयव्यक्तिविवरणं सम्पाद्यताम् अनुसरणार्थमनुरोधाः प्रच्छन्नप्रदेशाः @@ -373,13 +373,13 @@ मतम् %d बहूनि मतानि अपरं मतं युज्यताम् - ७ दिनानि - ३ दिनानि - १ दिनम् - ६ घण्टाः - १ घण्टा - ३० निमेषाः - ५ निमेषाः + ७ दिनानि + ३ दिनानि + १ दिनम् + ६ घण्टाः + १ घण्टा + ३० निमेषाः + ५ निमेषाः मतपेटिका सारणहावभावस्य संयुतनं पीठिकापरिवर्तनार्थं कार्यम् सूचनाशोधकं दृश्यताम् diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 68c27870..14af6584 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -1,7 +1,7 @@ Prihlásiť sa účtom Mastodon - Koncepty + Koncepty Odhlásiť sa Nastavenia Nastavenia účtu diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 70fda13d..3f969334 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -34,7 +34,7 @@ Blokirani uporabniki Zahteve za Sledenje Uredi svoj profil - Osnutki + Osnutki Licence \@%s Občutljiva vsebina @@ -415,13 +415,13 @@ Vedno razširite tute, označene z opozorilom o vsebini Dodaj anketo Anketa - 5 minut - 30 minut - 1 ura - 6 ur - 1 dan - 3 dni - 7 dni + 5 minut + 30 minut + 1 ura + 6 ur + 1 dan + 3 dni + 7 dni Dodaj izbiro Več izbir Izbira %d diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 033087a6..160b2ee5 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -36,7 +36,7 @@ Blockerade användare Följarförfrågningar Ändra din profil - Utkast + Utkast Licenser \@%s %s knuffade @@ -425,13 +425,13 @@ Sökning misslyckades Skapa en omröstning Omröstning - 5 minuter - 30 minuter - 1 timme - 6 timmar - 1 dag - 3 dagar - 7 dagar + 5 minuter + 30 minuter + 1 timme + 6 timmar + 1 dag + 3 dagar + 7 dagar Lägg till alternativ Flerval Val %d @@ -484,4 +484,20 @@ Din privata notering om detta kontot Det finns inga meddelanden. Meddelanden + Aviseringar när någon du följer skrivit en ny toot + Nya toots + någon som jag följer har skrivit en ny toot + %s skrev precis + Dölj kvantitativ information på profiler + Dölj kvantitativ information på inlägg + Begränsa tidslinje aviseringar + Ändra aviseringar + Information som kan påverka ditt välmående kommer att döljas. Detta inkluderar: +\n +\n- Favorisering/Knuff/Följaraviseringar +\n- Favorisering/Antal knuffar +\n- Följare/Inlägg på profiler +\n +\nPush-aviseringar påverkas inte, men du ändra dina aviseringinställningar manuellt. + Välmående \ No newline at end of file diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 0e5bf8ec..5d418817 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -29,7 +29,7 @@ தடைசெய்யபட்ட பயனர்கள் பின்பற்ற கோரிக்கை சுயவிவரத்தை திருத்த - வரைவுகள் + வரைவுகள் %s மேலேற்றப்பட்டது உணர்ச்சிகரமான உள்ளடக்கம் ஊடகம் மறைக்கப்பட்டது @@ -265,9 +265,9 @@ நேரடி தகவல் பட்டைகள் பொருத்தப்பட்டது - 1 நாள் - 3 நாட்கள் - 7 நாட்கள் + 1 நாள் + 3 நாட்கள் + 7 நாட்கள் விருப்பத்தைச் சேர் பின்பற்ற கோரிக்கை நீக்கு diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/app/src/main/res/values-te/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 6b8e77e2..a775defa 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -1,13 +1,13 @@ เพิ่มตัวเลือก - 7 วัน - 3 วัน - 1 วัน - 6 ชั่วโมง - 1 ชั่วโมง - 30 นาที - 5 นาที + 7 วัน + 3 วัน + 1 วัน + 6 ชั่วโมง + 1 ชั่วโมง + 30 นาที + 5 นาที โพล เปิดใช้งานการเลื่อนนิ้วเพื่อสลับระหว่างแท็บ แสดงตัวกรองการแจ้งเตือน @@ -431,7 +431,7 @@ ตั้งค่าบัญชี ตั้งค่า ออกจากระบบ - ฉบับร่าง + ฉบับร่าง ชื่นชอบ การยืนยันตัวตนกับเซิร์ฟเวอร์นั้นล้มเหลว Instance คือ\? @@ -455,4 +455,37 @@ ซ่อนการแจ้งเตือน ปิดเสียงการแจ้งเตือนจาก %s ซ่อนหัวข้อของแถบเครื่องมือด้านบน + ล้มเหลวในการส่งโพสต์นี้! + ข้อมูลบางอย่างที่อาจส่งผลต่อสุขภาพจิตของคุณจะถูกซ่อนไว้ซึ่งรวมถึง: +\n +\n- การแจ้งเตือน ชื่นชอบ/ดัน/ติดตาม +\n- จำนวนการ ชื่นชอบ/ดัน บนโพสต์ +\n- สถิติ ผู้ติดตาม/โพสต์ ในโปรไฟล์ +\n +\n การแจ้งเตือนแบบพุชจะไม่ได้รับผลกระทบ แต่คุณสามารถตรวจสอบการตั้งค่าการแจ้งเตือนได้ด้วยตนเอง + แจ้งเตือน Limit timeline + แจ้งเตือน Review + ใครบางคนที่ฉันได้ติดตาม ได้เผยแพร่โพสต์ใหม่ + ฟีเจอร์ฉบับร่างใน Tusky ได้รับการออกแบบใหม่ทั้งหมดเพื่อให้เร็วขึ้นเป็นมิตรกับผู้ใช้มากขึ้นและบั๊กน้อยลง +\n คุณยังสามารถเข้าถึงฉบับร่างเก่าผ่านปุ่มในหน้าฉบับร่างใหม่ แต่จะถูกลบออกในการอัปเดตในอนาคต! + ซ่อนสถิติเชิงปริมาณในโปรไฟล์ + ซ่อนสถิติเชิงปริมาณของโพสต์ + สุขภาวะ + บันทึกส่วนตัวของคุณเกี่ยวกับบัญชีนี้ + แจ้งเตือน เมื่อคนที่คุณติดตาม ได้เผยแพร่โพสต์ใหม่ + โพสต์ที่คุณได้ร่างตอบไว้ ถูกลบแลัว + ลบฉบับร่างแล้ว + ล้มเหลวในการโหลดข้อมูลตอบกลับ + ฉบับร่างเก่า + คุณต้องการลบลิสต์ %s ใช่ไหม\? + คุณไม่สามารถอัปโหลดไฟล์แนบมากกว่า %1$d ได้ + บันทึกแล้ว! + ไม่มีประกาศ + ไม่มีกำหนด + ระยะเวลา + ไฟล์แนบ + เสียง + โพสต์ใหม่ + %s พึ่งโพสต์ + ประกาศ \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 5f66d066..6e3a3e0c 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -36,7 +36,7 @@ Engellenmiş kullanıcılar Takip Etme İstekleri Profili düzeltme - Taslaklar + Taslaklar Lisanslar \@%s %s yineledi @@ -418,13 +418,13 @@ Hesaplar Arama başarısız Anket - 5 dakika - 30 dakika - 1 saat - 6 saat - 1 gün - 3 gün - 7 gün + 5 dakika + 30 dakika + 1 saat + 6 saat + 1 gün + 3 gün + 7 gün Seçenek ekle Çoklu seçim Düzenle diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index cb7869ad..5c49c828 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -37,7 +37,7 @@ Налаштування акаунта Налаштування Вийти - Чернетки + Чернетки Вподобане Увійти Зʼєднання… diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 71cb2057..9cebe1fc 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -124,8 +124,8 @@ Mở ngăn kéo Làm mờ hình ảnh Nhắc tới - Bỏ ẩn cuộc trò chuyện - Ẩn cuộc trò chuyện + Mở lại thông báo + Tắt thông báo Ẩn %s Bỏ ẩn Ẩn @@ -134,7 +134,7 @@ Tạo bình chọn Thêm tệp Mở trong trình duyệt - Bộ sưu tập + Album Yêu cầu theo dõi Máy chủ đã ẩn Người dùng đã chặn @@ -171,7 +171,7 @@ Thu gọn Xem thêm Thu gọn - Mở rộng + Xem thêm Hiển thị Nội dung bị ẩn Nhạy cảm @@ -188,7 +188,7 @@ Người theo dõi Theo dõi Ghim - Tương tác + Rép Tút Tút Xếp tab @@ -197,7 +197,7 @@ Cộng đồng Thông báo Bảng tin - Nháp + Nháp Lượt thích Máy chủ là gì\? Tải xem trước hình ảnh @@ -289,7 +289,7 @@ Cộng đồng xem thêm Trả lời @%s - Bộ sưu tập + Thư viện Luôn hiện nội dung bị ẩn Luôn hiện nội dung nhạy cảm Đang theo dõi bạn @@ -318,13 +318,13 @@ Lựa chọn %d Cho phép chọn nhiều lựa chọn Thêm lựa chọn - 7 ngày - 3 ngày - 1 ngày - 6 giờ - 1 giờ - 30 phút - 5 phút + 7 ngày + 3 ngày + 1 ngày + 6 giờ + 1 giờ + 30 phút + 5 phút Bình chọn Vuốt qua lại giữa các tab Hiện bộ lọc thông báo @@ -352,7 +352,7 @@ Cuộc bình chọn bạn tạo đã kết thúc Cuộc bình chọn của bạn đã kết thúc Bình chọn - Kết thúc + xong kết thúc lúc %s %s người @@ -420,7 +420,7 @@ Để sau Bạn cần khởi động lại Tusky để áp dụng các thiết lập Yêu cầu khởi động lại ứng dụng - Mở tút + Xem tút Mở rộng/Thu gọn toàn bộ tút Đang tìm kiếm… Bạn cần tải về bộ emoji này trước @@ -456,7 +456,39 @@ Bỏ ẩn %s Ẩn tiêu đề tab Đã lưu! - Ghi chú của bạn + Thêm ghi chú Chưa có thông báo. - Tin tức + Có gì mới\? + Ẩn số liệu trên trang cá nhân + Ẩn tương tác trên tút + Hạn chế thông báo trên bảng tin + Chọn loại thông báo + Các thông tin ảnh hưởng tới tâm lý hành vi của bạn sẽ bị ẩn. Bao gồm: +\n +\n - Thông báo Lượt thích/Chia sẻ/Theo dõi +\n - Số Lượt thích/Chia sẻ của tút +\n - Số Người theo dõi/Tút trên trang cá nhân +\n +\nThông báo đẩy sẽ không ảnh hưởng, bạn có thể tự thiết lập trong phần cài đặt điện thoại của bạn. + Cai nghiện + Thông báo khi người bạn đăng ký theo dõi đăng tút mới + Tút mới + người tôi đăng ký theo dõi đăng tút mới + %s vừa đăng tút + Bạn không thể đính kèm quá %1$d tệp. + Vĩnh viễn + Thời hạn + Bạn thật sự muốn xóa danh sách %s\? + Đính kèm + Âm thanh + Tút bạn lên lịch đã bị hủy bỏ + Tút lên lịch cũ + Tút lên lịch đã xóa + Chưa tải được bình luận + Tính năng lên lịch đăng tút của Tusky được thiết kế lại hoàn toàn để nhanh hơn, thân thiện hơn và ít lỗi hơn. +\nBạn vẫn có thể xem lại bản nháp cũ nhưng chúng sẽ bị xóa bỏ trong bản cập nhật tương lai! + Đăng tút không thành công! + Emoji động + Ngưng nhận thông báo + Nhận thông báo \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 740fdb57..0b688a9c 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -36,7 +36,7 @@ 被屏蔽的用户 关注请求 编辑个人资料 - 草稿 + 草稿 开源协议 \@%s %s 转嘟了 @@ -436,13 +436,13 @@ 搜索失败 显示通知过滤器 投票 - 5 分钟 - 30 分钟 - 1 小时 - 6 小时 - 1 天 - 3 天 - 7 天 + 5 分钟 + 30 分钟 + 1 小时 + 6 小时 + 1 天 + 3 天 + 7 天 添加选择 多项选择 选择 %d diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index f6db5a95..96bffb66 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -1,24 +1,24 @@ - 應用程式出現異常 - 網絡請求出錯,請檢查互聯網連接並重試 - 內容不能為空 + 應用程式出現異常。 + 網絡請求出錯,請檢查互聯網連接並重試! + 內容不能為空。 該域名無效 - 無法連接此伺服器 - 沒有可用的瀏覽器 - 認證過程出現未知錯誤 - 授權被拒絕 - 無法獲取登入資訊 + 無法連接此伺服器。 + 沒有可用的瀏覽器。 + 認證過程出現未知錯誤。 + 授權被拒絕。 + 無法獲取登入資訊。 嘟文太長了! - 檔案大小限制 8MB - 影片大小限制 40MB - 無法上傳此類型的檔案 - 此檔案無法開啟 - 需要授予 Tusky 讀取媒體檔案的權限 - 需要授予 Tusky 寫入儲存空間的權限 - 無法在嘟文中同時插入影片和圖片 - 媒體檔案上傳失敗 - 嘟文發送時出錯 + 檔案大小限制 8MB。 + 影片大小限制 40MB。 + 無法上傳此類型的檔案。 + 此檔案無法開啟。 + 需要授予 Tusky 讀取媒體檔案的權限。 + 需要授予 Tusky 寫入儲存空間的權限。 + 無法在嘟文中同時插入影片和圖片。 + 媒體檔案上傳失敗。 + 嘟文發送時出錯。 主頁 通知設定 本站時間軸 @@ -36,7 +36,7 @@ 被封鎖的使用者 關注請求 編輯個人資料 - 草稿 + 草稿 開源授權 \@%s %s 轉嘟了 @@ -47,10 +47,10 @@ 摺疊內容 展開 摺疊 - 沒有內容 - 還沒有內容,向下拉動即可重新整理 + 沒有內容。 + 還沒有內容,向下拉動即可重新整理! %s 轉嘟了你的嘟文 - %s 收藏了你的嘟文 + %s 把你的嘟文加入了最愛 %s 關注了你 檢舉使用者 @%s 的濫用行為 更多評論? @@ -112,12 +112,12 @@ 話題 打開轉嘟用戶主頁 顯示轉嘟 - 顯示收藏 + 顯示最愛 話題 提及 連結 打開媒體 #%d - 正在下載 %1$s… + 正在下載 %1$s 複製連結 打開為 %s 分享為 … @@ -130,8 +130,8 @@ 已解除封鎖 已解除靜音 已檢舉! - 成功送出回覆 - 域名 + 成功送出回覆。 + 哪一個域名? 有什麼新鮮事? 敏感內容警告 暱稱 @@ -143,8 +143,14 @@ 標題 什麼是站點? 正在連線… - 請輸入你帳號所在的 Mastodon 站點的域名或地址 - 正在完成上傳… + 輸入你帳號所在的 Mastodon 站點的域名或地址,譬如 mastodon.social、icosahedron.website、social.tchncs.de 和 更多 +\n +\n如果你還沒有帳號,你可以輸入你想要加入的域名並在此建立新帳號。 +\n +\n一個站點是一個託管你的帳號的地方,但是你可以很容易的跟不同站台的人們交流,就像是在同一個站台一樣。 +\n +\n更多資訊可以在 joinmastodon.org 查看。 + 正在完成上傳 正在上傳… 下載 移除關注請求? @@ -165,7 +171,7 @@ 被提及 有新的關注者 嘟文被轉嘟 - 嘟文被收藏 + 嘟文被加入收藏 投票已結束 外觀 佈景主題 @@ -210,7 +216,7 @@ 轉嘟 當有使用者轉嘟了我的嘟文時 收藏 - 當有使用者收藏了我的嘟文時 + 當有使用者把我的嘟文加入收藏時 投票 當我參與的投票結束時 %s 提及了你 @@ -323,7 +329,7 @@ 標籤 內容 嘟文顯示精確時間 - 以下資訊可能並不完整,要檢視完整資料請使用瀏覽器開啟 + 以下資訊可能並不完整,要檢視完整資料請使用瀏覽器開啟。 取消置頂 置頂 @@ -332,8 +338,8 @@ <b>%s</b> 次轉嘟 - 轉嘟 - 收藏 + 轉嘟由 + 收藏由 %1$s %1$s 和 %2$s %1$s, %2$s 和 %3$d 等人 @@ -402,4 +408,100 @@ 話題 關注請求 編輯 + 動態自訂表情符號 + 在隱藏的媒體上使用漸變色彩 + 動態 GIF 頭像 + 我關注的人有新嘟文 + 已送出關注請求 + 隱藏通知 + 靜音 @%s? + 封鎖 @%s? + 隱藏整個網域 + 確定要封鎖 %s 所有內容?你將不會在任何公開時間軸或是通知中看到來自這個網域的內容。你的關注者若來自這個網域則將會被移除。 + %s 已解除隱藏 + 重設 + 排程嘟文 + 排程的嘟文 + 取消靜音對話 + 靜音對話 + 取消靜音 %s + 靜音 %s + 靜音來自 %s 的通知 + 取消靜音來自 %s 的通知 + 取消靜音 %s + 新增投票 + 被隱藏的網域 + 被加入書籤 + 我的書籤 + 書籤 + 我的書籤 + %s 剛剛發了新嘟文 + %s 希望可以關注你 + 公告 + 已排程的嘟文 + 被隱藏的網域 + 聲音檔大小限制 40MB。 + 完整字詞 + 你的草稿欲回覆的原嘟文已被刪除 + 草稿已刪除 + 載入回覆資訊失敗 + 舊的草稿 + 這條嘟文發送失敗! + 你確定要刪除列表 %s? + 你無法上傳超過 %1$d 媒體附件。 + 已儲存! + 你對此帳號的個人註記 + 隱藏頂端工具列的標題 + 在轉嘟時提示確認 + 在時間軸中顯示連結預覽 + Mastodon 的最短發文間隔限制為 5 分鐘。 + 沒有公告。 + 你沒有任何已排程的嘟文。 + 你沒有任何草稿。 + 尋找嘟文時發生錯誤 %s + 選項 %d + 多個選項 + 新增選項 + 7 天 + 3 天 + 1 天 + 6 小時 + 1 小時 + 30 分鐘 + 5 分鐘 + 無限期 + 期間 + 投票 + 啟用在分頁間切換的滑動手勢 + 顯示通知過濾器 + 搜尋失敗 + 帳號 + 擷取狀態失敗 + 回報失敗 + 轉送至 %s + 額外的評論 + 成功回報 @%s + 完成 + 返回 + 繼續 + + %s 人 + + + 列表 + 選擇列表 + 加上話題標籤 + 投票選項: %1$s, %2$s, %3$s, %4$s; %5$s + Google 目前的表情符號包 + 總是顯示被標注為內容警告的嘟文 + 附件 + 錄音 + 由 Tusky 提供 + Tusky %s + 當你關注的人發布新嘟文時通知 + 新嘟文 + 關注請求的通知 + 底端 + 頂端 + 主要導覽列的位置 \ No newline at end of file diff --git a/app/src/main/res/values-zh-rMO/strings.xml b/app/src/main/res/values-zh-rMO/strings.xml index fcf97807..636757b0 100644 --- a/app/src/main/res/values-zh-rMO/strings.xml +++ b/app/src/main/res/values-zh-rMO/strings.xml @@ -36,7 +36,7 @@ 被封鎖的使用者 關注請求 編輯個人資料 - 草稿 + 草稿 開源授權 \@%s %s 轉嘟了 diff --git a/app/src/main/res/values-zh-rSG/strings.xml b/app/src/main/res/values-zh-rSG/strings.xml index 402f569c..ab7a89f4 100644 --- a/app/src/main/res/values-zh-rSG/strings.xml +++ b/app/src/main/res/values-zh-rSG/strings.xml @@ -36,7 +36,7 @@ 被屏蔽的用户 关注请求 编辑个人资料 - 草稿 + 草稿 开源协议 \@%s %s 转嘟了 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 856d7c92..45c170a8 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -1,24 +1,24 @@ - 應用程式出現異常 - 網絡請求出錯,請檢查互聯網連接並重試 - 內容不能為空 + 應用程式出現異常。 + 網絡請求出錯,請檢查互聯網連接並重試! + 內容不能為空。 該域名無效 - 無法連接此伺服器 - 沒有可用的瀏覽器 - 認證過程出現未知錯誤 - 授權被拒絕 - 無法獲取登入資訊 + 無法連接此伺服器。 + 沒有可用的瀏覽器。 + 認證過程出現未知錯誤。 + 授權被拒絕。 + 無法獲取登入資訊。 嘟文太長了! - 檔案大小限制 8MB - 影片大小限制 40MB - 無法上傳此類型的檔案 - 此檔案無法開啟 - 需要授予 Tusky 讀取媒體檔案的權限 - 需要授予 Tusky 寫入儲存空間的權限 - 無法在嘟文中同時插入影片和圖片 - 媒體檔案上傳失敗 - 嘟文發送時出錯 + 檔案大小限制 8MB。 + 影片大小限制 40MB。 + 無法上傳此類型的檔案。 + 此檔案無法開啟。 + 需要授予 Tusky 讀取媒體檔案的權限。 + 需要授予 Tusky 寫入儲存空間的權限。 + 無法在嘟文中同時插入影片和圖片。 + 媒體檔案上傳失敗。 + 嘟文發送時出錯。 主頁 通知 本站時間軸 @@ -36,7 +36,7 @@ 被封鎖的使用者 關注請求 編輯個人資料 - 草稿 + 草稿 開源授權 \@%s %s 轉嘟了 @@ -47,8 +47,8 @@ 摺疊內容 展開 摺疊 - 沒有內容 - 還沒有內容,向下拉動即可重新整理 + 沒有內容。 + 還沒有內容,向下拉動即可重新整理! %s 轉嘟了你的嘟文 %s 收藏了你的嘟文 %s 關注了你 @@ -112,12 +112,12 @@ 話題 打開轉嘟用戶主頁 顯示轉嘟 - 顯示收藏 + 顯示最愛 話題 提及 連結 打開媒體 #%d - 正在下載 %1$s… + 正在下載 %1$s 複製連結 打開為 %s 分享為 … @@ -130,8 +130,8 @@ 已解除封鎖 已解除靜音 已發送! - 成功送出回覆 - 域名 + 成功送出回覆。 + 哪一個域名? 有什麼新鮮事? 敏感內容警告 暱稱 @@ -143,8 +143,14 @@ 標題 什麼是站點? 正在連線… - 請輸入你帳號所在的 Mastodon 站點的域名或地址 - 正在完成上傳… + 輸入你帳號所在的 Mastodon 站點的域名或地址,譬如 mastodon.social、icosahedron.website、social.tchncs.de 和 更多 +\n +\n如果你還沒有帳號,你可以輸入你想要加入的域名並在此建立新帳號。 +\n +\n一個站點是一個託管你的帳號的地方,但是你可以很容易的跟不同站台的人們交流,就像是在同一個站台一樣。 +\n +\n更多資訊可以在 joinmastodon.org 查看。 + 正在完成上傳 正在上傳… 下載 移除關注請求? @@ -165,7 +171,7 @@ 被提及 有新的關注者 嘟文被轉嘟 - 嘟文被收藏 + 嘟文被加入收藏 投票已結束 外觀 佈景主題 @@ -210,7 +216,7 @@ 轉嘟 當有使用者轉嘟了我的嘟文時 收藏 - 當有使用者收藏了我的嘟文時 + 當有使用者把我的嘟文加入收藏時 投票 當我參與的投票結束時 %s 提及了你 @@ -323,7 +329,7 @@ 標籤 內容 嘟文顯示精確時間 - 以下資訊可能並不完整,要檢視完整資料請使用瀏覽器開啟 + 以下資訊可能並不完整,要檢視完整資料請使用瀏覽器開啟。 取消置頂 置頂 @@ -333,7 +339,7 @@ <b>%s</b> 次轉嘟 轉嘟 - 收藏 + 收藏由 %1$s %1$s 和 %2$s %1$s, %2$s 和 %3$d 等人 @@ -428,4 +434,92 @@ 編輯 書籤 音檔必需小於40MB。 + Tusky 的草稿功能已重新設計,更快、更好用、更少問題。 +\n 你還是可以在草稿頁面中查看你的先前的舊草稿,但它們在未來的某次更新中將會被移除! + 隱藏個人頁面中的狀態數量資訊 + 隱藏貼文上的狀態數量資訊 + 限制時間軸通知 + 檢查通知設定 + 有些資訊可能會影響你的心理健康將會被隱藏。包括: +\n +\n- 收藏/轉嘟/關注 通知 +\n- 收藏/轉嘟 數量 +\n- 關注/貼文 在個人頁面的狀態 +\n +\n推播通知不會受到影響,但你可以手動檢查你的通知設定。 + 數位健康 + + %s 人 + + + %s 剛剛發了新嘟文 + %s 請求關注你 + 動態自訂表情符號 + 你的草稿欲回覆的原嘟文已被刪除 + 草稿已刪除 + 載入回覆資訊失敗 + 舊的草稿 + 這條嘟文發送失敗! + 附件 + 錄音 + 你確定要刪除列表 %s? + 7 天 + 3 天 + 1 天 + 6 小時 + 1 小時 + 30 分鐘 + 5 分鐘 + 無限期 + 期間 + 你無法上傳超過 %1$d 媒體附件。 + 當你關注的人發布新嘟文時通知 + 新嘟文 + 我關注的人有新嘟文 + 沒有公告。 + 公告 + 已儲存! + 你對此帳號的個人註記 + 隱藏頂端工具列的標題 + 隱藏通知 + 靜音來自 %s 的通知 + 取消靜音來自 %s 的通知 + 取消靜音 %s + 取消靜音 %s + 底端 + 頂端 + 主要導覽列的位置 + 在隱藏的媒體上使用漸變色彩 + 加上話題標籤 + 在轉嘟時提示確認 + 在時間軸中顯示連結預覽 + 啟用在分頁間切換的滑動手勢 + 關注請求的通知 + 已送出關注請求 + 靜音 @%s? + 封鎖 @%s? + 取消靜音對話 + 靜音對話 + Mastodon 的最短發文間隔限制為 5 分鐘。 + 你沒有任何草稿。 + 你沒有任何已排程的嘟文。 + 列表 + 選擇列表 + 被加入書籤 + 我的書籤 + 書籤 + 由 Tusky 提供 + 尋找嘟文時發生錯誤 %s + 重設 + 排程嘟文 + 排程的嘟文 + 已排程的嘟文 + 選項 %d + 多個選項 + 新增選項 + 投票 + 新增投票 + 總是顯示被標注為內容警告的嘟文 + 搜尋失敗 + 帳號 \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index dd4f599c..5ec69307 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -45,6 +45,7 @@ 5dp 12dp + 120dp 72dp 108dp diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index ef219b2f..612c1fe3 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -60,12 +60,15 @@ Taqbaylit Tiếng Việt Türkçe + български Русский العربية + کوردیی ناوەندی বাংলা (বাংলাদেশ) বাংলা (ভারত) فارسی हिंदी + संस्कृतम् தமிழ் ภาษาไทย 한국어 @@ -103,12 +106,15 @@ kab vi tr + bg ru ar + ckb bn-bd bn-in fa hi + sa ta th ko @@ -145,13 +151,13 @@ - @string/poll_duration_5_min - @string/poll_duration_30_min - @string/poll_duration_1_hour - @string/poll_duration_6_hours - @string/poll_duration_1_day - @string/poll_duration_3_days - @string/poll_duration_7_days + @string/duration_5_min + @string/duration_30_min + @string/duration_1_hour + @string/duration_6_hours + @string/duration_1_day + @string/duration_3_days + @string/duration_7_days @@ -164,5 +170,27 @@ 604800 + + @string/duration_indefinite + @string/duration_5_min + @string/duration_30_min + @string/duration_1_hour + @string/duration_6_hours + @string/duration_1_day + @string/duration_3_days + @string/duration_7_days + + + + 0 + 300 + 1800 + 3600 + 21600 + 86400 + 259200 + 604800 + + <b>%1$d%%</b> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 08e3aaf6..b4f91410 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -40,7 +40,7 @@ Hidden domains Follow Requests Edit your profile - Drafts + Drafts Scheduled toots Announcements Licenses @@ -62,6 +62,7 @@ %s favorited your toot %s followed you %s requested to follow you + %s just posted Report @%s Additional comments? @@ -223,6 +224,7 @@ my posts are boosted my posts are favorited polls have ended + somebody I\'m subscribed to published a new toot Appearance App Theme Timelines @@ -241,6 +243,7 @@ Show indicator for bots Animate GIF avatars Show colorful gradients for hidden media + Animate custom emojis Timeline filtering Tabs @@ -287,7 +290,8 @@ Notifications when your toots get marked as favorite Polls Notifications about polls that have ended - + New toots + Notifications when somebody you\'re subscribed to published a new toot %s mentioned you %1$s, %2$s, %3$s and %4$d others @@ -323,6 +327,8 @@ Share link to toot Images Video + Audio + Attachments Follow requested @@ -555,13 +561,15 @@ Poll - 5 minutes - 30 minutes - 1 hour - 6 hours - 1 day - 3 days - 7 days + Duration + Indefinite + 5 minutes + 30 minutes + 1 hour + 6 hours + 1 day + 3 days + 7 days Add choice Multiple choices Choice %d @@ -575,9 +583,37 @@ Show link previews in timelines Show confirmation dialog before boosting Hide the title of the top toolbar + Wellbeing Your private note about this account Saved! - Register New Account + Some information that might affect your mental wellbeing will be hidden. This includes:\n\n + - Favorite/Boost/Follow notifications\n + - Favorite/Boost count on toots\n + - Follower/Post stats on profiles\n\n + Push-notifications will not be affected, but you can review your notification preferences manually. + + Review Notifications + Limit timeline notifications + Hide quantitative stats on posts + Hide quantitative stats on profiles + You cannot upload more than %1$d media attachments. + Do you really want to delete the list %s? + This toot failed to send! + + + The draft feature in Tusky has been completely redesigned to be faster, more user friendly and less buggy.\n + You can still access your old drafts via a button on the new drafts screen, + but they will be removed in a future update! + + Old Drafts + Failed loading Reply information + Draft deleted + The Toot you drafted a reply to has been removed + + Subscribe + Unsubscribe + + Register New Account diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index 7f33a976..bd9be3b2 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -13,7 +13,6 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ - package com.keylesspalace.tusky import android.content.Intent @@ -25,6 +24,7 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT import com.keylesspalace.tusky.components.compose.MediaUploader +import com.keylesspalace.tusky.components.drafts.DraftHelper import com.keylesspalace.tusky.db.* import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Account @@ -115,6 +115,7 @@ class ComposeActivityTest { accountManagerMock, mock(MediaUploader::class.java), mock(ServiceClient::class.java), + mock(DraftHelper::class.java), mock(SaveTootHelper::class.java), dbMock ) diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt index 73a00670..b603a4a7 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeTokenizerTest.kt @@ -44,6 +44,29 @@ class ComposeTokenizerTest(private val text: CharSequence, arrayOf(" @ment10n_ @ment20n_", 11, 20), arrayOf(" @ment10n_ @ment20n_n", 11, 21), arrayOf(" @ment10n_ @ment20n_9", 11, 21), + arrayOf(" @ment10n-", 1, 10), + arrayOf(" @ment10n- @", 11, 12), + arrayOf(" @ment10n- @ment20n", 11, 19), + arrayOf(" @ment10n- @ment20n-", 11, 20), + arrayOf(" @ment10n- @ment20n-n", 11, 21), + arrayOf(" @ment10n- @ment20n-9", 11, 21), + arrayOf("@ment10n@l0calhost", 0, 18), + arrayOf(" @ment10n@l0calhost", 1, 19), + arrayOf(" @ment10n_@l0calhost", 1, 20), + arrayOf(" @ment10n-@l0calhost", 1, 20), + arrayOf(" @ment10n_@l0calhost @ment20n@husky", 21, 35), + arrayOf(" @ment10n_@l0calhost @ment20n_@husky", 21, 36), + arrayOf(" @ment10n-@l0calhost @ment20n-@husky", 21, 36), + arrayOf(" @m@localhost", 1, 13), + arrayOf(" @m@localhost @a@localhost", 14, 26), + arrayOf("@m@", 0, 3), + arrayOf(" @m@ @a@asdf", 5, 12), + arrayOf(" @m@ @a@", 5, 8), + arrayOf(" @m@ @a@a", 5, 9), + arrayOf(" @m@a @a@m", 6, 10), + arrayOf("@m@m@", 5, 5), + arrayOf("#tusky@husky", 12, 12), + arrayOf(":tusky@husky", 12, 12), arrayOf("mention", 7, 7), arrayOf("ment10n", 7, 7), arrayOf("mentio_", 7, 7), diff --git a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt b/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt index 16d0e271..7a7c3f7d 100644 --- a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt @@ -28,6 +28,7 @@ import org.mockito.ArgumentMatchers.* import org.mockito.Mock import org.mockito.MockitoAnnotations import org.robolectric.annotation.Config +import retrofit2.Response import java.util.* import java.util.concurrent.TimeUnit import kotlin.collections.ArrayList @@ -76,8 +77,8 @@ class TimelineRepositoryTest { makeStatus("3"), makeStatus("2") ) - whenever(mastodonApi.homeTimelineSingle(isNull(), isNull(), anyInt())) - .thenReturn(Single.just(statuses)) + whenever(mastodonApi.homeTimeline(isNull(), isNull(), anyInt())) + .thenReturn(Single.just(Response.success(statuses))) val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.NETWORK) .blockingGet() @@ -107,8 +108,8 @@ class TimelineRepositoryTest { ) val sinceId = "2" val sinceIdMinusOne = "1" - whenever(mastodonApi.homeTimelineSingle(null, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(response)) + whenever(mastodonApi.homeTimeline(null, sinceIdMinusOne, limit + 1)) + .thenReturn(Single.just(Response.success(response))) val result = subject.getStatuses(null, sinceId, sinceIdMinusOne, limit, TimelineRequestMode.NETWORK) .blockingGet() @@ -141,8 +142,8 @@ class TimelineRepositoryTest { ) val sinceId = "2" val sinceIdMinusOne = "1" - whenever(mastodonApi.homeTimelineSingle(null, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(response)) + whenever(mastodonApi.homeTimeline(null, sinceIdMinusOne, limit + 1)) + .thenReturn(Single.just(Response.success(response))) val result = subject.getStatuses(null, sinceId, sinceIdMinusOne, limit, TimelineRequestMode.NETWORK) .blockingGet() @@ -181,8 +182,8 @@ class TimelineRepositoryTest { val sinceId = "2" val sinceIdMinusOne = "1" val maxId = "3" - whenever(mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(response)) + whenever(mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1)) + .thenReturn(Single.just(Response.success(response))) val result = subject.getStatuses(maxId, sinceId, sinceIdMinusOne, limit, TimelineRequestMode.NETWORK) .blockingGet() @@ -224,8 +225,8 @@ class TimelineRepositoryTest { val sinceId = "2" val sinceIdMinusOne = "1" val maxId = "4" - whenever(mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1)) - .thenReturn(Single.just(response)) + whenever(mastodonApi.homeTimeline(maxId, sinceIdMinusOne, limit + 1)) + .thenReturn(Single.just(Response.success(response))) val result = subject.getStatuses(maxId, sinceId, sinceIdMinusOne, limit, TimelineRequestMode.NETWORK) .blockingGet() @@ -263,8 +264,8 @@ class TimelineRepositoryTest { dbResult.status = dbStatus.toEntity(account.id, gson) dbResult.account = status.account.toEntity(account.id, gson) - whenever(mastodonApi.homeTimelineSingle(any(), any(), any())) - .thenReturn(Single.just(listOf(status))) + whenever(mastodonApi.homeTimeline(any(), any(), any())) + .thenReturn(Single.just(Response.success((listOf(status))))) whenever(timelineDao.getStatusesForAccount(account.id, status.id, null, 30)) .thenReturn(Single.just(listOf(dbResult))) val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.ANY) @@ -281,8 +282,8 @@ class TimelineRepositoryTest { val dbResult2 = TimelineStatusWithAccount() dbResult2.status = Placeholder("1").toEntity(account.id) - whenever(mastodonApi.homeTimelineSingle(any(), any(), any())) - .thenReturn(Single.just(listOf(status))) + whenever(mastodonApi.homeTimeline(any(), any(), any())) + .thenReturn(Single.just(Response.success(listOf(status)))) whenever(timelineDao.getStatusesForAccount(account.id, status.id, null, 30)) .thenReturn(Single.just(listOf(dbResult, dbResult2))) val result = subject.getStatuses(null, null, null, limit, TimelineRequestMode.ANY) diff --git a/fastlane/metadata/android/bg/changelogs/61.txt b/fastlane/metadata/android/bg/changelogs/61.txt new file mode 100644 index 00000000..c6fed811 --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Поддръжка за показване на анкети, гласуване и известия за анкети +- Нови бутони за филтриране на раздела за известия и за изтриване на всички известия +- изтриване и преработване на вашите собствени публикации +- нов индикатор, който показва дали даден акаунт е бот на изображението на профила (може да бъде изключен в предпочитанията) +- Нови преводи: норвежки, букмал и словенски. diff --git a/fastlane/metadata/android/bg/changelogs/67.txt b/fastlane/metadata/android/bg/changelogs/67.txt new file mode 100644 index 00000000..9cf27a89 --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Вече можете да създавате анкети от Tusky +- Подобрено търсене +- Нова опция в Предпочитания на акаунта за винаги разширяване на предупрежденията за съдържание +- Аватарите в навигационното чекмедже вече имат закръглена квадратна форма +- Вече е възможно да докладвате за потребители, дори когато те никога не са публикували статус +- Tusky сега ще откаже да се свързва чрез връзки с чист текст на Android 6+ +- Много други малки подобрения и корекции на грешки diff --git a/fastlane/metadata/android/bg/changelogs/68.txt b/fastlane/metadata/android/bg/changelogs/68.txt new file mode 100644 index 00000000..ae09bdf1 --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Тази версия осигурява съвместимост с Mastodon 3 и подобрява производителността и стабилността. diff --git a/fastlane/metadata/android/bg/changelogs/70.txt b/fastlane/metadata/android/bg/changelogs/70.txt new file mode 100644 index 00000000..d1cca33a --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Вече можете да маркирате състояния и да показвате отметките си в Tusky. +- Вече можете да планирате публикации с Tusky. Имайте предвид, че избраното време трябва да бъде поне 5 минути в бъдеще. +- Вече можете да добавяте списъци към главния екран. +- Вече можете да публикувате аудио прикачени файлове с Tusky. + +И много други малки подобрения и корекции на грешки! diff --git a/fastlane/metadata/android/bg/changelogs/74.txt b/fastlane/metadata/android/bg/changelogs/74.txt new file mode 100644 index 00000000..4bcdadaf --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Подобрен основен интерфейс - вече можете да премествате разделите отдолу +- Когато заглушавате потребител, вече можете също да решите дали да заглушите известията му +- Вече можете да следвате колкото искате хештегове в един единствен раздел хештегове +- Подобрен е начинът, по който се показват описанията на мултимедиите, така че да работи дори за супер дълги описания + +Пълен дневник на промените: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/bg/changelogs/77.txt b/fastlane/metadata/android/bg/changelogs/77.txt new file mode 100644 index 00000000..e24a0601 --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- поддръжка за бележки в профила (функция на Mastodon 3.2.0) +- поддръжка за администраторски съобщения (функция на Mastodon 3.1.0) + +- аватарът на избрания от вас акаунт вече ще се показва в главната лента с инструменти +- щракването върху показваното име в емисия ще отвори страницата с профила на този потребител + +- много корекции на грешки и малки подобрения +- подобрени преводи diff --git a/fastlane/metadata/android/bg/full_description.txt b/fastlane/metadata/android/bg/full_description.txt new file mode 100644 index 00000000..73ce4354 --- /dev/null +++ b/fastlane/metadata/android/bg/full_description.txt @@ -0,0 +1,12 @@ +Tusky е лек клиент за Mastodon, свободен сървър за социални мрежи с отворен код. + +• Материален дизайн +• Повечето приложени API на Mastodon +• Поддръжка на няколко акаунта +• Тъмна и светла тема с възможност за автоматично превключване в зависимост от часа +• Чернови - съставете публикации и ги запазете за по-късно +• Изберете между различни стилове емоджита +• Оптимизиран за всички размери на екрана +• Напълно отворен код - няма несвободни зависимости като услугите на Google + +За да научите повече за Mastodon, посетете https://joinmastodon.org/ diff --git a/fastlane/metadata/android/bg/short_description.txt b/fastlane/metadata/android/bg/short_description.txt new file mode 100644 index 00000000..d0331150 --- /dev/null +++ b/fastlane/metadata/android/bg/short_description.txt @@ -0,0 +1 @@ +Клиент с няколко акаунта за социалната мрежа Mastodon diff --git a/fastlane/metadata/android/bg/title.txt b/fastlane/metadata/android/bg/title.txt new file mode 100644 index 00000000..0238ffc0 --- /dev/null +++ b/fastlane/metadata/android/bg/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/ckb/changelogs/77.txt b/fastlane/metadata/android/ckb/changelogs/77.txt new file mode 100644 index 00000000..ee35b258 --- /dev/null +++ b/fastlane/metadata/android/ckb/changelogs/77.txt @@ -0,0 +1,10 @@ +تاسکی وشانی ١٣.٠ + +- پشتگیری بۆ تێبینیەکانی پرۆفایل (تایبەتمەندی ماستۆدۆن ٣.٢.٠) +- پشتگیری لە راگەیاندنی بەڕێوەبەر (تایبەتمەندی ماستۆدۆن ٣.١.٠) + +- ئێستا ئەژمێری هەڵبژێردراوی هەژمارەکەت لە شریتی ئامڕازی سەرەکی دا پیشان دەدرێت +- کرتە کردن لەسەر ناوی پیشاندان لە هێڵی کات ئێستا لاپەڕەی پرۆفایلی ئەو بەکارهێنەرە هەڵدەدات + +- زۆر چاککردنەوەی هەڵەکان و چاککردنەوەی بچووک +- وەرگێڕانە باشەکان diff --git a/fastlane/metadata/android/ckb/full_description.txt b/fastlane/metadata/android/ckb/full_description.txt new file mode 100644 index 00000000..ab90d927 --- /dev/null +++ b/fastlane/metadata/android/ckb/full_description.txt @@ -0,0 +1,12 @@ +توسکی ئەپێکی سووکەڵە بۆ ماستۆدۆنە، خزمەتکاری تۆڕی کۆمەڵایەتی ئازاد و کراوە + +• دیزاینی ماتریالی +• زۆربەی ماستۆدۆن API جێبەجێ دەکا +• پشتیوانی هەژمارەی هەمەجۆر +• ڕووکاری تاریک و رووناک لەگەڵ ئەگەری گۆڕینی خۆکار لەسەر بنەمای کاتی رۆژ +• ڕەشنووسەکان - دروستکردنی دووتەکان و هەڵگرتنیان بۆ دواتر +• هەڵبژێرە لەنێوان شێوازە ئیمۆجییە جیاوازەکان +• باشترکراوە بۆ هەموو قەبارەی شاشە +• بەتەواوی کراوەی سەرچاوە - هیچ پشت پێبەستنێکی نائازاد وەک خزمەتگوزاریەکانی گووگڵ + +بۆ زیاتر فێربوون لەبارەی مەستوورن ، سەردانی https://joinmastodon.org/ diff --git a/fastlane/metadata/android/ckb/short_description.txt b/fastlane/metadata/android/ckb/short_description.txt new file mode 100644 index 00000000..df2f8d34 --- /dev/null +++ b/fastlane/metadata/android/ckb/short_description.txt @@ -0,0 +1 @@ +کڕیارێکی هەژماری هەمەجۆر بۆ تۆڕی کۆمەڵایەتی ماستۆدۆن diff --git a/fastlane/metadata/android/ckb/title.txt b/fastlane/metadata/android/ckb/title.txt new file mode 100644 index 00000000..57a4e890 --- /dev/null +++ b/fastlane/metadata/android/ckb/title.txt @@ -0,0 +1 @@ +تاسکی diff --git a/fastlane/metadata/android/en-US/changelogs/80.txt b/fastlane/metadata/android/en-US/changelogs/80.txt new file mode 100644 index 00000000..14d28f0a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Get notified when a followed user posts - click the bell icon on their profile! (Mastodon 3.3.0 feature) +- The draft feature in Tusky has been completely redesigned to be faster, more user friendly and less buggy. +- A new wellbeing mode that allows you to limit certain Tusky features has been added. +- Tusky can now animate custom emojis. +Full changelog: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/fr/changelogs/58.txt b/fastlane/metadata/android/fr/changelogs/58.txt index fb6993c1..22c4090e 100644 --- a/fastlane/metadata/android/fr/changelogs/58.txt +++ b/fastlane/metadata/android/fr/changelogs/58.txt @@ -1,3 +1,6 @@ Tusky v6.0 -- +- Les filtres de la timeline ont été déplacés dans les Préférences de compte et se synchronisent avec le serveur +- Vous pouvez personnaliser un onglet avec un hashtag dans la fenêtre principale +- Les listes peuvent être éditées +- Sécurité : suppression du support pour TLS 1.0 et TLS 1.1, et ajout du support pour TLS 1.3 sur Android 6+ diff --git a/fastlane/metadata/android/fr/changelogs/70.txt b/fastlane/metadata/android/fr/changelogs/70.txt index 832aec90..5fb322c6 100644 --- a/fastlane/metadata/android/fr/changelogs/70.txt +++ b/fastlane/metadata/android/fr/changelogs/70.txt @@ -1,7 +1,7 @@ Tusky v10.0 - Vous pouvez maintenant marquer les statuts et lister vos signets dans Tusky. -- Vous pouvez maintenant programmer des toots avec Tusky. Notez que l'heure que vous sélectionnez doit être d'au moins 5 minutes dans le futur. +- Vous pouvez maintenant programmer des pouets avec Tusky. Notez que l'heure que vous sélectionnez doit être d'au moins 5 minutes dans le futur. - Vous pouvez maintenant ajouter des listes à l'écran principal. - Vous pouvez désormais publier des pièces jointes audio avec Tusky. diff --git a/fastlane/metadata/android/fr/changelogs/72.txt b/fastlane/metadata/android/fr/changelogs/72.txt index ad75c990..fb48ccbc 100644 --- a/fastlane/metadata/android/fr/changelogs/72.txt +++ b/fastlane/metadata/android/fr/changelogs/72.txt @@ -2,8 +2,8 @@ Tusky v11.0 - Notifications à propos des nouvelles demandes d’abonnement lorsque votre compte est verrouillé - Nouvelles fonctionalités activables via l’écran Préférences : -- désactiver le pivotement entre onglets -- afficher une boîte de dialogue de confirmation avant de booster un pouet -- afficher les aperçus des liens dans les fils + - désactiver le pivotement entre onglets + - afficher une boîte de dialogue de confirmation avant de booster un pouet + - afficher les aperçus des liens dans les fils - Possibilité de mettre en sourdine les conversations - Les résultats des sondages seront désormais calculés en fonction du nombre des sond·é·s diff --git a/fastlane/metadata/android/fr/changelogs/74.txt b/fastlane/metadata/android/fr/changelogs/74.txt new file mode 100644 index 00000000..f61f152a --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Amélioration de l'interface principale - vous pouvez maintenant déplacer les onglets vers le bas +- Lorsque vous mettez un utilisateur en sourdine, vous pouvez désormais décider de désactiver ses notifications +- Vous pouvez maintenant suivre autant de hashtags que vous le souhaitez dans un seul onglet hashtag +- La description des médias s'affiche correctement quelque soit sa taille + +Historique complet : https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/fr/changelogs/77.txt b/fastlane/metadata/android/fr/changelogs/77.txt new file mode 100644 index 00000000..1558ac22 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- prise en charge des notes de profil (fonctionnalité de Mastodon 3.2.0) +- le support des annonces de l'administration (fonctionnalité de Mastodon 3.1.0) + +- l'avatar de votre compte sélectionné apparaîtra désormais dans la barre d'outils principale +- en cliquant sur le nom affiché dans une timeline, la page de profil de cet utilisateur s'ouvrira + +- plein corrections de bugs et de petites améliorations +- l'amélioration des traductions diff --git a/fastlane/metadata/android/hu/changelogs/80.txt b/fastlane/metadata/android/hu/changelogs/80.txt new file mode 100644 index 00000000..dd5d5804 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Értesítést kaphatsz, amikor egy követett felhasználó tülköl - csak kattints a csengő ikonra a profilján! (Mastodon 3.3.0 funkció) +- A Tusky piszkozat funkcióját teljesen újraterveztük, hogy gyorsabb, felhasználóbarátabb, hibamentesebb legyen. +- Az új jóllét üzemmód lehetővé teszi, hogy bizonyos Tusky funkciókat korlátozz. +- A Tusky mostantól képes animálni az egyedi emojikat is. +Összes változás: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/is/changelogs/74.txt b/fastlane/metadata/android/is/changelogs/74.txt new file mode 100644 index 00000000..f178690a --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky útg. 12.0 + +- Bætt aðalviðmót - hægt að færa flipa neðst +- Þegar þaggað er niður í notanda er núna hægt að ákveða hvort líka sé þaggað niður í tilkynningum frá honum +- Nú er hægt að fylgjast með eins mörgum myllumerkjum og maður vill í hverjum myllumerkis-flipa +- Bett leið við að birta lýsingar á myndefni, þannig að núna virka mjög langar lýsingar + +Full breytingaskrá: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/nb_NO/changelogs/80.txt b/fastlane/metadata/android/nb_NO/changelogs/80.txt new file mode 100644 index 00000000..8e4b8595 --- /dev/null +++ b/fastlane/metadata/android/nb_NO/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Mulighet for å bli varslet dersom en bruker du følger publiserer en ny toot - trykk på bjelle-ikonet på profilen deres (krever Mastodon 3.3.0) +- Ny og forbedret kladd-funksjonalitet. +- Velværemodus: Kan brukes til å begrense utvalgt funksjonalitet i Tusky. Du kan aktivere velværemodus i innstillinger. +- Støtte for animerte emojis. Dette er skrudd av som standard, men du kan skru det på i innstillinger. +- Komplett endringelogg: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/vi/changelogs/80.txt b/fastlane/metadata/android/vi/changelogs/80.txt new file mode 100644 index 00000000..3d54a1a9 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/80.txt @@ -0,0 +1,9 @@ +Tusky v14.0 + +- Thông báo khi người bạn theo dõi đăng tút - click vào biểu tượng cái chuông trên trang cá nhân của họ! (Mastodon 3.3.0) +- Tút Nháp: được thiết kế lại toàn bộ, giúp nhanh hơn, dễ dùng hơn và ít lỗi hơn. +- Chế độ Cai Nghiện: cho phép bạn giới hạn một số tính năng của Tusky. +- Hỗ trợ Emoji động: cho phép xem emoji động trong Tusky. +- Ẩn Có Thời Hạn: có thể chặn người nào đó trong khoảng thời gian cho trước. +- Sửa các lỗi vặt, đặc biệt là sự tương thích Pleroma. +- Cải thiện bản dịch diff --git a/gradle.properties b/gradle.properties index 8144ece0..bada7909 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,6 +14,8 @@ org.gradle.jvmargs=-Xmx4096m # use parallel execution org.gradle.parallel=true +# enable file system watching +org.gradle.vfs.watch=true android.enableR8.fullMode=true android.useAndroidX=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 62d4c053..e708b1c0 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 33682bbb..1c4bcc29 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index fbd7c515..4f906e0c 100755 --- a/gradlew +++ b/gradlew @@ -130,7 +130,7 @@ fi if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath diff --git a/gradlew.bat b/gradlew.bat index a9f778a7..ac1b06f9 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -54,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -64,21 +64,6 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line @@ -86,7 +71,7 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell