diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/47.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/47.json
new file mode 100644
index 000000000..ba5202639
--- /dev/null
+++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/47.json
@@ -0,0 +1,995 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 47,
+ "identityHash": "308b3faf2255729075a85abab23a1c9e",
+ "entities": [
+ {
+ "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, `failedToSendNew` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)",
+ "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
+ },
+ {
+ "fieldPath": "failedToSendNew",
+ "columnName": "failedToSendNew",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "scheduledAt",
+ "columnName": "scheduledAt",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "language",
+ "columnName": "language",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "statusId",
+ "columnName": "statusId",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "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, `clientId` TEXT, `clientSecret` TEXT, `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, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT 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, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` 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": "clientId",
+ "columnName": "clientId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "clientSecret",
+ "columnName": "clientSecret",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "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": "notificationsSignUps",
+ "columnName": "notificationsSignUps",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationsUpdates",
+ "columnName": "notificationsUpdates",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "notificationsReports",
+ "columnName": "notificationsReports",
+ "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": "defaultPostLanguage",
+ "columnName": "defaultPostLanguage",
+ "affinity": "TEXT",
+ "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
+ },
+ {
+ "fieldPath": "oauthScopes",
+ "columnName": "oauthScopes",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "unifiedPushUrl",
+ "columnName": "unifiedPushUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pushPubKey",
+ "columnName": "pushPubKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pushPrivKey",
+ "columnName": "pushPrivKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pushAuth",
+ "columnName": "pushAuth",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pushServerKey",
+ "columnName": "pushServerKey",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "id"
+ ],
+ "autoGenerate": true
+ },
+ "indices": [
+ {
+ "name": "index_AccountEntity_domain_accountId",
+ "unique": true,
+ "columnNames": [
+ "domain",
+ "accountId"
+ ],
+ "orders": [],
+ "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, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, 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": "minPollDuration",
+ "columnName": "minPollDuration",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxPollDuration",
+ "columnName": "maxPollDuration",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "charactersReservedPerUrl",
+ "columnName": "charactersReservedPerUrl",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "version",
+ "columnName": "version",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "videoSizeLimit",
+ "columnName": "videoSizeLimit",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "imageSizeLimit",
+ "columnName": "imageSizeLimit",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "imageMatrixLimit",
+ "columnName": "imageMatrixLimit",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxMediaAttachments",
+ "columnName": "maxMediaAttachments",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxFields",
+ "columnName": "maxFields",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxFieldNameLength",
+ "columnName": "maxFieldNameLength",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "maxFieldValueLength",
+ "columnName": "maxFieldValueLength",
+ "affinity": "INTEGER",
+ "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, `editedAt` INTEGER, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, `quote` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
+ "fields": [
+ {
+ "fieldPath": "serverId",
+ "columnName": "serverId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "timelineUserId",
+ "columnName": "timelineUserId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "authorServerId",
+ "columnName": "authorServerId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "inReplyToId",
+ "columnName": "inReplyToId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "inReplyToAccountId",
+ "columnName": "inReplyToAccountId",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "editedAt",
+ "columnName": "editedAt",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "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": "repliesCount",
+ "columnName": "repliesCount",
+ "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": true
+ },
+ {
+ "fieldPath": "visibility",
+ "columnName": "visibility",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "attachments",
+ "columnName": "attachments",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "mentions",
+ "columnName": "mentions",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "tags",
+ "columnName": "tags",
+ "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
+ },
+ {
+ "fieldPath": "expanded",
+ "columnName": "expanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentCollapsed",
+ "columnName": "contentCollapsed",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentShowing",
+ "columnName": "contentShowing",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pinned",
+ "columnName": "pinned",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "card",
+ "columnName": "card",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "language",
+ "columnName": "language",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "quote",
+ "columnName": "quote",
+ "affinity": "TEXT",
+ "notNull": false
+ }
+ ],
+ "primaryKey": {
+ "columnNames": [
+ "serverId",
+ "timelineUserId"
+ ],
+ "autoGenerate": false
+ },
+ "indices": [
+ {
+ "name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
+ "unique": false,
+ "columnNames": [
+ "authorServerId",
+ "timelineUserId"
+ ],
+ "orders": [],
+ "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, `order` INTEGER 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_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` 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_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))",
+ "fields": [
+ {
+ "fieldPath": "accountId",
+ "columnName": "accountId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "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.editedAt",
+ "columnName": "s_editedAt",
+ "affinity": "INTEGER",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lastStatus.emojis",
+ "columnName": "s_emojis",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.favouritesCount",
+ "columnName": "s_favouritesCount",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.repliesCount",
+ "columnName": "s_repliesCount",
+ "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.tags",
+ "columnName": "s_tags",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lastStatus.showingHiddenContent",
+ "columnName": "s_showingHiddenContent",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.expanded",
+ "columnName": "s_expanded",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.collapsed",
+ "columnName": "s_collapsed",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.muted",
+ "columnName": "s_muted",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastStatus.poll",
+ "columnName": "s_poll",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "lastStatus.language",
+ "columnName": "s_language",
+ "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, '308b3faf2255729075a85abab23a1c9e')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 77f81aba4..932ba219d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -39,7 +39,18 @@
+ android:windowSoftInputMode="adjustResize"
+ android:exported="true">
+
+
+
+
+
+
+
+
,
poll: NewPoll?,
failedToSend: Boolean,
+ failedToSendAlert: Boolean,
scheduledAt: String?,
language: String?,
statusId: String?,
@@ -123,6 +124,7 @@ class DraftHelper @Inject constructor(
attachments = attachments,
poll = poll,
failedToSend = failedToSend,
+ failedToSendNew = failedToSendAlert,
scheduledAt = scheduledAt,
language = language,
statusId = statusId,
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
index 7cdcd2597..6d9a2aa16 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt
@@ -33,6 +33,7 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.databinding.ActivityDraftsBinding
import com.keylesspalace.tusky.db.DraftEntity
+import com.keylesspalace.tusky.db.DraftsAlert
import com.keylesspalace.tusky.di.ViewModelFactory
import com.keylesspalace.tusky.util.parseAsMastodonHtml
import com.keylesspalace.tusky.util.visible
@@ -46,6 +47,9 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
@Inject
lateinit var viewModelFactory: ViewModelFactory
+ @Inject
+ lateinit var draftsAlert: DraftsAlert
+
private val viewModel: DraftsViewModel by viewModels { viewModelFactory }
private lateinit var binding: ActivityDraftsBinding
@@ -83,6 +87,9 @@ class DraftsActivity : BaseActivity(), DraftActionListener {
adapter.addLoadStateListener {
binding.draftsErrorMessageView.visible(adapter.itemCount == 0)
}
+
+ // If a failed post is saved to drafts while this activity is up, do nothing; the user is already in the drafts view.
+ draftsAlert.observeInContext(this, false)
}
override fun onOpenDraft(draft: DraftEntity) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt
index fd93a01ce..c5ad6e79c 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt
@@ -21,6 +21,7 @@ import android.content.SharedPreferences
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.util.Log
+import android.view.Menu
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
@@ -37,6 +38,7 @@ import com.keylesspalace.tusky.di.Injectable
import com.keylesspalace.tusky.entity.AccessToken
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.getNonNullString
+import com.keylesspalace.tusky.util.openLinkInCustomTab
import com.keylesspalace.tusky.util.viewBinding
import kotlinx.coroutines.launch
import okhttp3.HttpUrl
@@ -64,24 +66,8 @@ class LoginActivity : BaseActivity(), Injectable {
is LoginResult.Ok -> lifecycleScope.launch {
fetchOauthToken(result.code)
}
- is LoginResult.Err -> {
- // Authorization failed. Put the error response where the user can read it and they
- // can try again.
- setLoading(false)
- // Use error returned by the server or fall back to the generic message
- binding.domainTextInputLayout.error =
- result.errorMessage.ifBlank { getString(R.string.error_authorization_denied) }
- Log.e(
- TAG,
- "%s %s".format(
- getString(R.string.error_authorization_denied),
- result.errorMessage
- )
- )
- }
- is LoginResult.Cancel -> {
- setLoading(false)
- }
+ is LoginResult.Err -> displayError(result.errorMessage)
+ is LoginResult.Cancel -> setLoading(false)
}
}
@@ -114,7 +100,7 @@ class LoginActivity : BaseActivity(), Injectable {
getString(R.string.preferences_file_key), Context.MODE_PRIVATE
)
- binding.loginButton.setOnClickListener { onButtonClick() }
+ binding.loginButton.setOnClickListener { onLoginClick(true) }
binding.whatsAnInstanceTextView.setOnClickListener {
val dialog = AlertDialog.Builder(this)
@@ -125,13 +111,9 @@ class LoginActivity : BaseActivity(), Injectable {
textView?.movementMethod = LinkMovementMethod.getInstance()
}
- if (isAdditionalLogin() || isAccountMigration()) {
- setSupportActionBar(binding.toolbar)
- supportActionBar?.setDisplayHomeAsUpEnabled(true)
- supportActionBar?.setDisplayShowTitleEnabled(false)
- } else {
- binding.toolbar.visibility = View.GONE
- }
+ setSupportActionBar(binding.toolbar)
+ supportActionBar?.setDisplayHomeAsUpEnabled(isAdditionalLogin() || isAccountMigration())
+ supportActionBar?.setDisplayShowTitleEnabled(false)
}
override fun requiresLogin(): Boolean {
@@ -145,12 +127,23 @@ class LoginActivity : BaseActivity(), Injectable {
}
}
+ override fun onCreateOptionsMenu(menu: Menu?): Boolean {
+ menu?.add(R.string.action_browser_login)?.apply {
+ setOnMenuItemClickListener {
+ onLoginClick(false)
+ true
+ }
+ }
+
+ return super.onCreateOptionsMenu(menu)
+ }
+
/**
* Obtain the oauth client credentials for this app. This is only necessary the first time the
* app is run on a given server instance. So, after the first authentication, they are
* saved in SharedPreferences and every subsequent run they are simply fetched from there.
*/
- private fun onButtonClick() {
+ private fun onLoginClick(openInWebView: Boolean) {
binding.loginButton.isEnabled = false
binding.domainTextInputLayout.error = null
@@ -183,7 +176,7 @@ class LoginActivity : BaseActivity(), Injectable {
.putString(CLIENT_SECRET, credentials.clientSecret)
.apply()
- redirectUserToAuthorizeAndLogin(domain, credentials.clientId)
+ redirectUserToAuthorizeAndLogin(domain, credentials.clientId, openInWebView)
},
{ e ->
binding.loginButton.isEnabled = true
@@ -197,10 +190,10 @@ class LoginActivity : BaseActivity(), Injectable {
}
}
- private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String) {
+ private fun redirectUserToAuthorizeAndLogin(domain: String, clientId: String, openInWebView: Boolean) {
// To authorize this app and log in it's necessary to redirect to the domain given,
// login there, and the server will redirect back to the app with its response.
- val url = HttpUrl.Builder()
+ val uri = HttpUrl.Builder()
.scheme("https")
.host(domain)
.addPathSegments(MastodonApi.ENDPOINT_AUTHORIZE)
@@ -209,13 +202,59 @@ class LoginActivity : BaseActivity(), Injectable {
.addQueryParameter("response_type", "code")
.addQueryParameter("scope", OAUTH_SCOPES)
.build()
- doWebViewAuth.launch(LoginData(domain, url.toString().toUri(), oauthRedirectUri.toUri()))
+ .toString()
+ .toUri()
+
+ if (openInWebView) {
+ doWebViewAuth.launch(LoginData(domain, uri, oauthRedirectUri.toUri()))
+ } else {
+ openLinkInCustomTab(uri, this)
+ }
}
override fun onStart() {
super.onStart()
- // first show or user cancelled login
+
+ /* Check if we are resuming during authorization by seeing if the intent contains the
+ * redirect that was given to the server. If so, its response is here! */
+ val uri = intent.data
+
+ if (uri?.toString()?.startsWith(oauthRedirectUri) == true) {
+ // This should either have returned an authorization code or an error.
+ val code = uri.getQueryParameter("code")
+ val error = uri.getQueryParameter("error")
+
+ /* restore variables from SharedPreferences */
+ val domain = preferences.getNonNullString(DOMAIN, "")
+ val clientId = preferences.getNonNullString(CLIENT_ID, "")
+ val clientSecret = preferences.getNonNullString(CLIENT_SECRET, "")
+
+ if (code != null && domain.isNotEmpty() && clientId.isNotEmpty() && clientSecret.isNotEmpty()) {
+ lifecycleScope.launch {
+ fetchOauthToken(code)
+ }
+ } else {
+ displayError(error)
+ }
+ } else {
+ // first show or user cancelled login
+ setLoading(false)
+ }
+ }
+
+ private fun displayError(error: String?) {
+ // Authorization failed. Put the error response where the user can read it and they
+ // can try again.
setLoading(false)
+
+ binding.domainTextInputLayout.error = if (error == null) {
+ // This case means a junk response was received somehow.
+ getString(R.string.error_authorization_unknown)
+ } else {
+ // Use error returned by the server or fall back to the generic message
+ Log.e(TAG, "%s %s".format(getString(R.string.error_authorization_denied), error))
+ error.ifBlank { getString(R.string.error_authorization_denied) }
+ }
}
private suspend fun fetchOauthToken(code: String) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt
index 0d804dd97..cf1dd4384 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt
@@ -36,7 +36,6 @@ import com.keylesspalace.tusky.util.CryptoUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.unifiedpush.android.connector.UnifiedPush
-import retrofit2.HttpException
private const val TAG = "PushNotificationHelper"
@@ -210,10 +209,8 @@ suspend fun updateUnifiedPushSubscription(context: Context, api: MastodonApi, ac
suspend fun unregisterUnifiedPushEndpoint(api: MastodonApi, accountManager: AccountManager, account: AccountEntity) {
withContext(Dispatchers.IO) {
api.unsubscribePushNotifications("Bearer ${account.accessToken}", account.domain)
- .onFailure {
- Log.d(TAG, "Error unregistering push endpoint for account " + account.id)
- Log.d(TAG, Log.getStackTraceString(it))
- Log.d(TAG, (it as HttpException).response().toString())
+ .onFailure { throwable ->
+ Log.w(TAG, "Error unregistering push endpoint for account " + account.id, throwable)
}
.onSuccess {
Log.d(TAG, "UnifiedPush unregistration succeeded for account " + account.id)
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 2b4ed61f7..ceeeba2c7 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
+++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
@@ -31,7 +31,7 @@ import java.io.File;
*/
@Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class
- }, version = 46)
+ }, version = 47)
public abstract class AppDatabase extends RoomDatabase {
public abstract AccountDao accountDao();
@@ -640,4 +640,11 @@ public abstract class AppDatabase extends RoomDatabase {
database.execSQL("ALTER TABLE `DraftEntity` ADD COLUMN `statusId` TEXT");
}
};
+
+ public static final Migration MIGRATION_46_47 = new Migration(46, 47) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ database.execSQL("ALTER TABLE `DraftEntity` ADD COLUMN `failedToSendNew` INTEGER NOT NULL DEFAULT 0");
+ }
+ };
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt
index 8029dd236..7b1f62b8c 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftDao.kt
@@ -15,6 +15,7 @@
package com.keylesspalace.tusky.db
+import androidx.lifecycle.LiveData
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
@@ -30,6 +31,12 @@ interface DraftDao {
@Query("SELECT * FROM DraftEntity WHERE accountId = :accountId ORDER BY id ASC")
fun draftsPagingSource(accountId: Long): PagingSource
+ @Query("SELECT COUNT(*) FROM DraftEntity WHERE accountId = :accountId AND failedToSendNew = 1")
+ fun draftsNeedUserAlert(accountId: Long): LiveData
+
+ @Query("UPDATE DraftEntity SET failedToSendNew = 0 WHERE accountId = :accountId AND failedToSendNew = 1")
+ suspend fun draftsClearNeedUserAlert(accountId: Long)
+
@Query("SELECT * FROM DraftEntity WHERE accountId = :accountId")
suspend fun loadDrafts(accountId: Long): List
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt
index d5f9edc9b..0b38385ae 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt
@@ -40,6 +40,7 @@ data class DraftEntity(
val attachments: List,
val poll: NewPoll?,
val failedToSend: Boolean,
+ val failedToSendNew: Boolean,
val scheduledAt: String?,
val language: String?,
val statusId: String?,
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftsAlert.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftsAlert.kt
new file mode 100644
index 000000000..917305d19
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftsAlert.kt
@@ -0,0 +1,99 @@
+/* Copyright 2023 Andi McClure
+ *
+ * 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.content.Context
+import android.content.DialogInterface
+import android.util.Log
+import androidx.appcompat.app.AlertDialog
+import androidx.lifecycle.LifecycleCoroutineScope
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.components.drafts.DraftsActivity
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * This class manages an alert popup when a post has failed and been saved to drafts.
+ * It must be separately registered in each lifetime in which it is to appear,
+ * and it only appears if the post failure belongs to the current user.
+ */
+
+private const val TAG = "DraftsAlert"
+
+@Singleton
+class DraftsAlert @Inject constructor(db: AppDatabase) {
+ // For tracking when a media upload fails in the service
+ private val draftDao: DraftDao = db.draftDao()
+
+ @Inject
+ lateinit var accountManager: AccountManager
+
+ public fun observeInContext(context: T, showAlert: Boolean) where T : Context, T : LifecycleOwner {
+ accountManager.activeAccount?.let { activeAccount ->
+ val coroutineScope = context.lifecycleScope
+
+ // Assume a single MainActivity, AccountActivity or DraftsActivity never sees more then one user id in its lifetime.
+ val activeAccountId = activeAccount.id
+
+ // This LiveData will be automatically disposed when the activity is destroyed.
+ val draftsNeedUserAlert = draftDao.draftsNeedUserAlert(activeAccountId)
+
+ // observe ensures that this gets called at the most appropriate moment wrt the context lifecycle—
+ // at init, at next onResume, or immediately if the context is resumed already.
+ if (showAlert) {
+ draftsNeedUserAlert.observe(context) { count ->
+ Log.d(TAG, "User id $activeAccountId changed: Notification-worthy draft count $count")
+ if (count > 0) {
+ AlertDialog.Builder(context)
+ .setTitle(R.string.action_post_failed)
+ .setMessage(
+ context.getResources().getQuantityString(R.plurals.action_post_failed_detail, count)
+ )
+ .setPositiveButton(R.string.action_post_failed_show_drafts) { _: DialogInterface?, _: Int ->
+ clearDraftsAlert(coroutineScope, activeAccountId) // User looked at drafts
+
+ val intent = DraftsActivity.newIntent(context)
+ context.startActivity(intent)
+ }
+ .setNegativeButton(R.string.action_post_failed_do_nothing) { _: DialogInterface?, _: Int ->
+ clearDraftsAlert(coroutineScope, activeAccountId) // User doesn't care
+ }
+ .show()
+ }
+ }
+ } else {
+ draftsNeedUserAlert.observe(context) { _ ->
+ Log.d(TAG, "User id $activeAccountId: Clean out notification-worthy drafts")
+ clearDraftsAlert(coroutineScope, activeAccountId)
+ }
+ }
+ } ?: run {
+ Log.w(TAG, "Attempted to observe drafts, but there is no active account")
+ }
+ }
+
+ /**
+ * Clear drafts alert for specified user
+ */
+ fun clearDraftsAlert(coroutineScope: LifecycleCoroutineScope, id: Long) {
+ coroutineScope.launch {
+ draftDao.draftsClearNeedUserAlert(id)
+ }
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt
index cb38752f7..c00e3b51d 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt
@@ -73,7 +73,8 @@ rb.emojis as 'rb_emojis', rb.bot as 'rb_bot'
FROM TimelineStatusEntity s
LEFT JOIN TimelineAccountEntity a ON (s.timelineUserId = a.timelineUserId AND s.authorServerId = a.serverId)
LEFT JOIN TimelineAccountEntity rb ON (s.timelineUserId = rb.timelineUserId AND s.reblogAccountId = rb.serverId)
-WHERE s.serverId = :statusId OR s.reblogServerId = :statusId"""
+WHERE (s.serverId = :statusId OR s.reblogServerId = :statusId)
+AND s.authorServerId IS NOT NULL"""
)
abstract suspend fun getStatus(statusId: String): TimelineStatusWithAccount?
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 91d355fa1..85ec3a4be 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
@@ -73,7 +73,7 @@ class AppModule {
AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38,
AppDatabase.MIGRATION_38_39, AppDatabase.MIGRATION_39_40, AppDatabase.MIGRATION_40_41,
AppDatabase.MIGRATION_41_42, AppDatabase.MIGRATION_42_43, AppDatabase.MIGRATION_43_44,
- AppDatabase.MIGRATION_44_45, AppDatabase.MIGRATION_45_46,
+ AppDatabase.MIGRATION_44_45, AppDatabase.MIGRATION_45_46, AppDatabase.MIGRATION_46_47
)
.build()
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt
index 566aa6472..b3c0246de 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt
@@ -98,6 +98,9 @@ class ViewVideoFragment : ViewMediaFragment() {
binding.mediaDescription.visible(showingDescription)
binding.mediaDescription.movementMethod = ScrollingMovementMethod()
+ // Ensure the description is visible over the video
+ binding.mediaDescription.elevation = binding.videoView.elevation + 1
+
binding.videoView.transitionName = url
binding.videoView.setVideoPath(url)
mediaController = object : MediaController(mediaActivity) {
diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt
index 6851f9c66..62433a823 100644
--- a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt
@@ -266,7 +266,7 @@ class SendStatusService : Service(), Injectable {
mediaUploader.cancelUploadScope(*failedStatus.media.map { it.localId }.toIntArray())
- saveStatusToDrafts(failedStatus)
+ saveStatusToDrafts(failedStatus, failedToSendAlert = true)
val notification = buildDraftNotification(
R.string.send_post_notification_error_title,
@@ -289,7 +289,7 @@ class SendStatusService : Service(), Injectable {
val sendJob = sendJobs.remove(statusId)
sendJob?.cancel()
- saveStatusToDrafts(statusToCancel)
+ saveStatusToDrafts(statusToCancel, failedToSendAlert = false)
val notification = buildDraftNotification(
R.string.send_post_notification_cancel_title,
@@ -306,7 +306,7 @@ class SendStatusService : Service(), Injectable {
}
}
- private suspend fun saveStatusToDrafts(status: StatusToSend) {
+ private suspend fun saveStatusToDrafts(status: StatusToSend, failedToSendAlert: Boolean) {
draftHelper.saveDraft(
draftId = status.draftId,
accountId = status.accountId,
@@ -320,6 +320,7 @@ class SendStatusService : Service(), Injectable {
mediaFocus = status.media.map { it.focus },
poll = status.poll,
failedToSend = true,
+ failedToSendAlert = failedToSendAlert,
scheduledAt = status.scheduledAt,
language = status.language,
statusId = status.statusId,
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
index 398359eaf..647e9bf36 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
@@ -252,7 +252,7 @@ private fun openLinkInBrowser(uri: Uri?, context: Context) {
* @param uri the uri to open
* @param context context
*/
-private fun openLinkInCustomTab(uri: Uri, context: Context) {
+fun openLinkInCustomTab(uri: Uri, context: Context) {
val toolbarColor = MaterialColors.getColor(context, R.attr.colorSurface, Color.BLACK)
val navigationbarColor = MaterialColors.getColor(context, android.R.attr.navigationBarColor, Color.BLACK)
val navigationbarDividerColor = MaterialColors.getColor(context, R.attr.dividerColor, Color.BLACK)
diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml
index eacf2d148..f10347385 100644
--- a/app/src/main/res/values-cy/strings.xml
+++ b/app/src/main/res/values-cy/strings.xml
@@ -3,11 +3,11 @@
Bu gwall.
Ni all hwn fod yn wag.
Rhoddwyd parth annilys
- Methu awdurdodi gyda\'r gweinydd hwnnw.
+ Methu awdurdodi gyda\'r gweinydd hwnnw. Os bydd hyn yn parhau, ceisiwch Mewngofnodi yn Porwr o\'r ddewislen.
Methu dod o hyd i borwr gwe i\'w ddefnyddio.
- Bu gwall awdurdodi anhysbys.
- Gwrthodwyd awdurdodi.
- Methu cael tocyn mewngofnodi.
+ Bu gwall awdurdodi anhysbys. Os bydd hyn yn parhau, ceisiwch Mewngofnodi yn Porwr o\'r ddewislen.
+ Gwrthodwyd awdurdodi. Os ydych chi\'n siŵr dy fod di wedi gyflenwi\'r manylion cywir, ceisiwch Mewngofnodi yn Porwr o\'r ddewislen.
+ Methu cael tocyn mewngofnodi. Os bydd hyn yn parhau, ceisiwch Mewngofnodi yn Porwr o\'r ddewislen.
Mae\'ch neges yn rhy hir!
Ni allwch lwytho\'r math hwnnw o ffeil.
Nid oedd modd agor y ffeil honno.
@@ -52,7 +52,7 @@
Ffefryn
Mwy
Creu
- Mewngofnodi â Mastodon
+ Mewngofnodi â Thusky
Allgofnodi
Ydych chi\'n siŵr eich bod am allgofnodi o\'r cyfrif %1$s?
Dilyn
@@ -487,7 +487,7 @@
Adroddodd %s %s
%s · %d post wedi\'u hatodi
mae yna adroddiad newydd
- Torri rheolau
+ Toriad rheol
Sbam
Wedi methu pinio
Wedi methu dadbinio
@@ -657,4 +657,13 @@
Rhannu URL cyfrif i…
Rhannu enw denyddiwr cyfrif i…
Enw defnyddiwr wedi\'i gopïo
+ Hynaf yn gyntaf
+ Diweddaraf yn gyntaf
+ Trefn darllen
+ Analluogwyd
+ <heb ei osod>
+ <annilys>
+ Mewngofnodi â phorwr
+ Yn gweithio yn y rhan mwyaf o achosion. Nid oes unrhyw ddata yn cael ei ollwng i apiau eraill.
+ Gall gefnogi dulliau dilysu ychwanegol, ond mae angen porwr a gefnogir.
diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml
index ca71032bf..c915346de 100644
--- a/app/src/main/res/values-eu/strings.xml
+++ b/app/src/main/res/values-eu/strings.xml
@@ -3,11 +3,11 @@
Errorea gertatu da.
Eremu hau ezin da hutsik egon.
Domeinu baliogabea sartu da
- Akatsa saioa hasterakoan.
+ Akatsa instantzia horrekin autentikatzerakoan. Akatsak badarrai, menutik, nabigatzailean saioa hasteko aukerarekin saiatu.
Ez da web nabigatzailerik aurkitu.
- Identifikatu gabeko baimentza akatsa gertatu da.
- Akatsa baimentzerakoan.
- Akatsa login identifikatzailea lortzerakoan.
+ Identifikatu gabeko baimen-akatsa gertatu da. Akatsak badarrai, menutik, nabigatzailean saioa hasteko aukerarekin saiatu.
+ Baimena ukatu da. Ziur bazaude zuk sartutako egiaztagiriak zuzenak direla, menutik, nabigatzailean saioa hasteko aukerarekin saiatu.
+ Akatsa saio-hasieraren identifikatzailea eskuratzerakoan. Akatsak badarrai, menutik, nabigatzailean saioa hasteko aukerarekin saiatu.
Tut luzeegia!
Ez da fitxategi mota hau onartzen.
Ezin izan da fitxategi hau ireki.
@@ -32,18 +32,18 @@
Profila editatu
Zirriborroak
Lizentziak
- %s(e)k bultzatu du
- Kontuz edukiarekin
- Ezkutuko multimedia
+ %s-(e)k bultzatu du
+ Eduki hunkigarria
+ Ezkutuko media
Sakatu ikusteko
- Gehiago erakutsi
+ Gehiago erakutsi
Gutxiago erakutsi
Zabaldu
- Bildu
+ Itxi
Edukirik ez. Arrastatu behera birkargatzeko!
%s(e)k zure tuta bultzatu du
%s(e)k zure tuta gogoko du
- %s(e)k jarraitu zaitu
+ %s-(e)k jarraitu zaitu
\@%s salatu
Informazio gehigarria?
Erantzun azkarra
@@ -97,7 +97,7 @@
Emoji teklatua
%1$s jaisten
Lotura kopiatu
- Tutaren URLa partekatu…
+ Tutaren URL partekatu…
Tuta partekatu…
Partekatu media hona…
Bidalia!
@@ -127,9 +127,9 @@
Mediaren igoera bukatzen
Igotzen…
Jaitsi
- Jarraipen-eskakizunari uko egin\?
+ Jarraipenaren eskakizunari uko egin\?
Kontu hau jarraitzeari utzi\?
- Tuta ezabatu\?
+ Tut hau ezabatu\?
Publikoa: Istorio publikoetan erakutsi
Ezkutukoa: Ez erakutsi istorio publikoetan
Pribatua: Jarraitzaileentzat soilik ikusgai
@@ -140,7 +140,7 @@
Soinuarekin jakinarazi
Bibrazioarekin jakinarazi
Led-arekin jakinarazi
- Jakinarazi noiz
+ Honen arabera jakinarazi
Aipatzen naute
Jarraitzen didate
Bultzatzen naute
@@ -154,8 +154,8 @@
Automatikoa
Nabigatzailea
Chromeko fitxak erabili
- Tut egiteko botoia ezkutatu beherantz joaterakoan
- Denbora-lerro filtroak
+ Tut berria idazteko botoia ezkutatu beherantz joan einean
+ Denbora-lerroaren iragaztea
Fitxak
Bultzadak erakutsi
Erakutsi erantzunak
@@ -172,10 +172,10 @@
Publiko
Zerrendagabetuta
Jarraitzaileak soilik
- Status testuaren tamaina
- Oso txikia
+ Tutaren testuaren tamaina
+ Txikiena
Txikia
- Erdikoa
+ Ertaina
Handia
Handiena
Aipamen berriak
@@ -186,7 +186,7 @@
Bultzatutako tuten jakinarazpenak
Gogokoak
Zure tutak gogoko bezala ezartzerakoan jakinarazpenak
- %s(e)k aipatu zaitu
+ %s-(e)k aipatu zaitu
%1$s, %2$s, %3$s eta beste %4$d
%1$s, %2$s eta %3$s
%1$s eta %2$s
@@ -211,10 +211,10 @@
Akatsen berri-emateak eta hobekuntza-eskariak:
\n https://github.com/accelforce/Yuito/issues
Yuitoren profila
- Partekatu tutaren edukia
- Partekatu tutaren lotura
+ Tutaren edukia partekatu
+ Tutaren esteka partekatu
Irudiak
- Bideoak
+ Bideoa
Eskaera bidalita
%du-an
@@ -244,18 +244,18 @@
Jarraitzaileak eskuz onartu beharko dituzu
Zirriborroa gorde?
Tuta bidaltzen…
- Errorea tuta bidaltzerakoan
+ Errorea tuta bidaltzean
Tuta bidaltzen
- Bidalketa ezeztatua
- Tutaren kopia zirriborroetan sartu da
+ Bidalketa bertan behera utzita
+ Tutaren kopia bat zure zirriborroetan gorde da
Idatzi
%s instantziak ez ditu emoji pertsonalizatuak eskaintzen
Emojien estiloa
Sistema
Lehenago jaitsi beharko dituzu
Bilatzen…
- Tut guztiak ezkutatu/zabaldu
- Ireki
+ Tut guztiak zabaldu/itxi
+ Tuta ireki
Berrabiaraztea beharrezkoa da
Aplikazioa berrabiarazi beharko duzu aldaketa ezartzeko
Beranduago
@@ -281,7 +281,7 @@
Sareko errore bat sortu da! Zure konexioa ziurta ezazu berriro, mesedez!
Mezu Zuzenak
Fitxak
- Lotuta
+ Finkatua
Ezkutuko domeinuak
Programatutako tutak
\@%s
@@ -310,7 +310,7 @@
Media jaisten
%s ez dago ezkutatua
Tut hau ezabatu eta zirriborro berria egin\?
- Ziur al zaude %s ezabatu nahi duzula\? Domeinu horretatik datorren edukia ez duzu denbora-lerro publikoetan edo jakinarazpenetan ikusiko. Domeinu horretan dituzun jarraitzaileak ezabatuko dira.
+ Ziur al zaude %s-(r)en eduki guztia ezabatu nahi duzula\? Domeinu horretatik datorren edukia ez duzu denbora-lerro publikoetan edo jakinarazpenetan ikusiko. Domeinu horretan dituzun jarraitzaileak ezabatuko dira.
Domeinu osoa ezkutatu
Galdeketak bukatu dira
Iragazkiak
@@ -367,12 +367,12 @@
- gehienezko %1$d fitxa iritsita
Media: %s
- Edukiaren abisua: %s
+ Edukiarekiko abisua: %s
Deskribapenik ez
- Birblogeatuta
- Gogotuta
- Publiko
- Zerrendagabetuta
+ Partekatua
+ Gogokoa
+ Publikoa
+ Zerrendatu gabea
Jarraitzaileak
Zuzena
Inkestatu aukerekin: %1$s, %2$s, %3$s, %4$s; %5$s
@@ -414,7 +414,7 @@
Iruzkin gehigarriak
%s(r)i birbidali
Txostena huts egin du
- Egoeren eskuratzea huts egin du
+ Akatsa tutak eskuratzean
Txostena zure zerbitzariaren moderatzaileari bidaliko zaio. Jarraian, kontu honen zergatia salatzen duzun azalpena eman dezakezu:
Kontua beste zerbitzari batekoa da. Bidali txostenaren kopia anonimatua hara ere\?
Kontuak
@@ -438,7 +438,7 @@
Laster-markak
Ireki bultzadaren egilea
Denbora lerro publikoak
- Laster-markatuta
+ Laster-marketara gehitua
Aukeratu zerrenda
Zerrenda
Ez duzu zirriborrorik.
@@ -448,8 +448,8 @@
Jarraitzeko eskaereri buruzko jakinarazpenak
\@%s isildu\?
\@%s blokeatu\?
- Mututu elkarrizketa
- %s(e)k zu jarraitzeko eskatu dizu
+ Elkarrizketa mututu
+ %s-(e)k zu jarraitzeko eskatu zaitu
Traolak
Jakinarazpenak ezkutatu
Desmututu %s
@@ -461,13 +461,13 @@
- Minutu %d faltan
- %d minutu faltan
- Gehitu traola
- Azpia
- Goia
- Nabigatze posizio nagusia
- Erakutsi gradiente koloretsua ezkutuko mediarentzako
- jarraipen-eskaera
- Desmututu elkarrizketa
+ Traola gehitu
+ Beheran
+ Goian
+ Nabigazio nagusiaren posizioa
+ Gradiente koloretsuak erakutsi ezkutuko mediaren lekuan
+ jarraitzeko eskaera jasotzean
+ Elkarrizketa desmututu
Desmututu %s
Jakinarazpenak berrikusi
Erakutsi baieztapen elkarrizketa-koadroa gogokoenetara gehitu aurretik
@@ -514,4 +514,13 @@
\nPush-jakinarazpenek ez dute eraginik izango, baina jakinarazpenen hobespenak eskuz berrikus ditzakezu.
Mezuetan estatistika kuantitatiboak ezkutatu
Laster-marka kendu
+ Ezin izan da irudia editatu.
+ Bideo eta audio fitxategiek ezin dute %s MBeko tamaina baino handiagoa izan.
+ Akatsa #%s mututzerakoan
+ Errorea #%s desmututzerakoan
+ Instantzia honek traolak jarraitzeko funtzioarekin bateragarritasuna ez dauka.
+ Akatsa #%s jarraitzerakoan
+ Akatsa #%s jarraitzerakoan
+ Ezin izan da saio-hasierako orria kargatu.
+ Akatsa kontuaren xehetasunak kargatzerakoan
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 64eb01c46..8bc0572f1 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -3,11 +3,11 @@
エラーが発生しました。
本文なしでは投稿できません。
無効なドメインです
- そのインスタンスでの認証に失敗しました。
+ そのインスタンスでの認証に失敗しました。失敗が続く場合、メニューからブラウザでのログインを試してください。
ウェブブラウザが見つかりませんでした。
- 不明な承認エラーが発生しました。
- 承認が拒否されました。
- ログイントークンの取得に失敗しました。
+ 不明な承認エラーが発生しました。失敗が続く場合、メニューからブラウザでのログインを試してください。
+ 認証が拒否されました。正しい認証情報を入力したことが確かな場合、メニューからブラウザでのログインを試してください。
+ ログイントークンの取得に失敗しました。もし失敗が続く場合、メニューからブラウザでのログインを試してください。
投稿文が長すぎます!
その形式のファイルはアップロードできません。
ファイルを開けませんでした。
@@ -58,7 +58,7 @@
引用
その他
投稿する
- Mastodonでログイン
+ TUsky でログイン
ログアウト
アカウント %1$s からログアウトしてもよろしいですか?
フォローする
@@ -592,4 +592,37 @@
アカウントのユーザー名を共有…
ユーザー名がコピーされました
簡易投稿欄を表示
+ スレッドの読み込み中
+ 新しい順
+ 読む順番
+ 古い順
+ サムネイル画像で常に表示される中心点を設定するには、円をタップまたはドラッグして中してくだだい。
+ 通知のミュート
+ %1$s に参加
+ %1$s 編集 %2$s
+ %1$s の投稿 %2$s
+ 投稿 %s の検索エラー
+ 中心点の設定に失敗しました
+ アカウントがロックされていなかったとしても、%1$s のスタッフは以下のアカウントのフォローリクエストを確認した方がいいと判断しました。
+ 中心点の設定
+ 保存していない変更があります。
+ サーバーからステータスの元情報を取得できませんでした。
+ 無効
+ <設定なし>
+ <無効>
+ やり取りした投稿が編集された時
+ やり取りした投稿が編集されたときの通知
+ ポートは %d から %d の間でなければなりません
+ アップロードに失敗しました
+ アップロードに失敗した投稿は下書きに保存されました。
+\n
+\nサーバーと接続できなかったか、投稿が拒否されました。
+ 下書きを表示
+ 閉じる
+ ブラウザでログイン
+ ほとんどの場合に動作します。他のアプリにはデータが漏洩しません。
+ 追加の認証方法がサポートされる可能性がありますが、対応ブラウザが必要です。
+ アップロードに失敗した投稿は下書きに保存されました。
+\n
+\nサーバーと接続できなかったか、投稿が拒否されました。
diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml
index cd6a6437d..7889e894a 100644
--- a/app/src/main/res/values-lv/strings.xml
+++ b/app/src/main/res/values-lv/strings.xml
@@ -440,4 +440,62 @@
Slēpt profilu kvantitatīvo statistiku
Slēpt ierakstu kvantitatīvo statistiku
Neizdevās ielādēt atbildes informāciju
+ Dzēst un sākt no jauna
+ Aizvākt
+ Vai atcelt sekošanas pieprasījumu\?
+ Vai dzēst šo ierakstu un sākt no jauna\?
+ Darbību nodrošina Tusky
+ Meklēt personas, kurām seko
+ Sūtot ziņu, radās kļūda
+ %1$s ir pārcēlies uz:
+ Piespraušana neizdevās
+ Atspraušana neizdevās
+ Dalīties ar ieraksta saturu
+ Pieskaries vai velc apli, lai izvēlētos fokusa punktu, kas vienmēr būs redzams sīktēlos.
+ Izplest/sakļaut visus ierakstus
+ Mastodon standarta emocijzīmju komplekts
+ Google aktuālais emocijzīmju komplekts
+ Paziņojumi par moderēšanas ziņojumiem
+ %s pieminēja tevi
+ %1$s un %2$s
+ %1$s, %2$s un %3$s
+ Dalīties ar saiti uz ierakstu
+ Neizdevās pievienot parakstu
+ Neizdevās iestatīt fokusa punktu
+ Izmantot absolūto laiku
+ %1$s
+ %1$s un %2$s
+ Satura brīdinājums: %s
+ Projekta vietne:
+\n https://tusky.app
+ Ielādē pavedienu
+ pārtraukta sekošana #%s
+ %s atcelta slēpšana
+ Nevarēja izveidot sarakstu
+ Lasīšanas secība
+ Filtrējamā frāze
+ Atspējots
+ <nav iestatīts>
+ <nederīgs>
+ %s (%s)
+ Pievienot jaunu Mastodon kontu
+ Animēt pielāgotās emocijzīmes
+ Jaunu dalībnieku reģistrācija
+ Kad pievienoti vairāki konti
+ Nepieciešama lietotnes restartēšana
+ Pastiprināt sākotnējai auditorijai
+ Pastiprināts
+ Atbildot @%s
+ Prasa manuāli apstiprināt sekotājus
+ Pastiprināja
+
+ - %s pastiprinājumi
+ - %s pastiprinājums
+ - %s pastiprinājumi
+
+
+ - sasniegts maksimālais ciļņu skaits %1$d
+ - sasniegts maksimālais ciļņu skaits %1$d
+ - sasniegts maksimālais ciļņu skaits %1$d
+
\ 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 1329c22fb..ff28b4714 100644
--- a/app/src/main/res/values-nl/strings.xml
+++ b/app/src/main/res/values-nl/strings.xml
@@ -60,7 +60,7 @@
Favoriet verwijderen
Meer
Bericht schrijven
- Aanmelden
+ Aanmelden met Mastodon
Afmelden
Ben je er zeker van dat je het account %1$s wil afmelden?
Volgen
@@ -328,14 +328,10 @@
Geen omschrijving
Geboost
Als favoriet gemarkeerd
- Openbaar
-
- Minder openbaar
-
- Volgers
-
- Direct
-
+ Openbaar
+ Minder openbaar
+ Volgers
+ Direct
Media downloaden
Media aan het downloaden
Filters
@@ -561,4 +557,39 @@
Fout tijdens het volgen van #%s
Fout tijdens het ontvolgen van #%s
Dit ingeplande bericht verwijderen\?
+ Leesvolgorde
+ Oudste eerst
+ Nieuwste eerst
+ Account toevoegen aan de lijst is mislukt
+ Toevoegen of verwijderen van lijst
+ Kan niet vastmaken
+ Uitgeschakeld
+ Account verwijderen van de lijst is mislukt
+ Er zijn niet opgeslagen wijzigingen.
+ Meldingen negeren
+ Bewerkingen
+ Standaardtaal van berichten
+ Rapporten
+ Bewerkt
+ %1$s bewerkte %2$s
+ %1$s maakte %2$s
+ Door in te loggen ben je het eens met de regels van %s.
+ Spam
+ Overig
+ Media moet een beschrijving hebben.
+ Kan niet losmaken
+ %s regels
+ %s (%s)
+ Regelovertreding
+ Je hebt geen lijsten.
+ nu
+ ALT
+ Ontvolg #%s\?
+ Fout bij negeren #%s
+ Ga door met bewerken
+ Er is een nieuw rapport
+ Nieuw rapport over %s
+ Gebruikersnaam gekopieerd
+ #%s ontvolgd
+ Gevolgde hashtags
\ 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 4f7034e78..3a07e4915 100644
--- a/app/src/main/res/values-oc/strings.xml
+++ b/app/src/main/res/values-oc/strings.xml
@@ -601,4 +601,16 @@
Ignorar las modificacions
Téner de modificar
Avètz de modificacions pas salvadas.
+ Cargament del fil
+ Òrdre de lectura
+ Mai ancians en primièr
+ Mai recents primièr
+ Desactivat
+ <pas definit>
+ <invalid>
+ Partejar lo ligam al compte
+ Partejar lo nom d’utilizaire del compte
+ Partejar l’URL del compte amb…
+ Partejar lo nom d’utilizaire del compte amb…
+ Nom d’utilizaire copiat
diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml
index 18eff7575..b0f6d2682 100644
--- a/app/src/main/res/values-pl/strings.xml
+++ b/app/src/main/res/values-pl/strings.xml
@@ -3,9 +3,9 @@
Wystąpił błąd.
To nie może pozostać puste.
Wprowadzono nieprawidłową domenę
- Nie udało się uwierzytelnić z tą instancją.
+ Nie udało się uwierzytelnić z tą instancją. Jeśli problem będzie się powtarzał, spróbuj zalogować się za pomocą przeglądarki z menu aplikacji.
Nie znaleziono przeglądarki internetowej.
- Wystąpił nieokreślony błąd podczas próby autoryzacji.
+ Wystąpił nieokreślony błąd podczas próby autoryzacji. Jeśli problem będzie się powtarzał, spróbuj zalogować się za pomocą przeglądarki z menu aplikacji.
Odmówiono autoryzacji.
Nie udało się uzyskać tokenu logowania.
Zbyt długi wpis!
@@ -54,7 +54,7 @@
Wyloguj się
Czy na pewno chcesz wylogować się z konta %1$s?
Obserwuj
- Odobserwuj
+ Przestań śledzić
Zablokuj
Odblokuj
Ukryj podbicia
@@ -598,4 +598,29 @@
Czy chcesz zapisać jako szkic\? (Załączniki zostaną załadowane ponownie po przywróceniu szkicu.)
Ta instancja nie wspiera obserwowania hashtagów.
Obserwowane hashtagi
+ Ładowanie wątku
+ Logując się akceptujesz regulamin %s.
+ Najpierw najstarsze
+ Najpierw najnowsze
+ Nie masz żadnych list.
+ Wycisz powiadomienia
+ %1$s edytował %2$s
+ %1$s stworzył %2$s
+ Edycje
+ Masz niezapisane zmiany.
+ %s regulamin
+ Błąd wysyłania
+ Pokaż szkice
+ Odrzuć
+ ALT
+ Zaloguj się przez przeglądarkę
+ Kontynuuj edycję
+ Spam
+ Udostępnij link do konta…
+ Udostępnij nazwę użytkownika…
+ Skopiowano nazwę użytkownika
+ Edytowano
+ Inne
+ Edytowano %s
+ Usunąć ten zaplanowany wpis\?
\ No newline at end of file
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 204e09717..396dd4ea0 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -20,7 +20,7 @@
Notificações
Linha local
Linha global
- Mensagens Diretas
+ Mensagens diretas
Editar abas
Conversa
Toots
@@ -54,7 +54,7 @@
Desfavoritar
Mais
Compor
- Entrar com Mastodon
+ Entrar com Tusky
Sair
Tem certeza de que deseja sair da conta %1$s\?
Seguir
@@ -518,4 +518,22 @@
180 dias
14 dias
365 dias
+ Desabilitado
+ <não definido>
+ <inválido>
+ Sempre
+ A imagem não pôde ser editada.
+ Arquivos de vídeo e áudio não podem exceder %s MB de tamanho.
+ A mídia deve ter uma descrição.
+ Nunca
+ ALT
+ Erro ao silenciar #%s
+ Entrar com Navegador
+ Nome de usuário copiado
+ Hashtags seguidas
+ Detalhes
+ Erro ao seguir #%s
+ Erro ao deixar de seguir #%s
+ Não foi possível carregar a página de login.
+ Falha ao carregar detalhes da conta
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 4d12960bb..4e4e4a89a 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -36,7 +36,7 @@
Вийти
Чернетки
Вподобане
- Увійти з Mastodon
+ Увійти з Tusky
Зʼєднання…
Немає результатів
Пошук…
@@ -139,10 +139,10 @@
Тред
Вкладки
Не вдалося відвантажити.
- Не вдалося отримати токен входу.
- Авторизацію відхилено.
- Сталася помилка невпізнання авторизації.
- Помилка автентифікації цього сервера.
+ Не вдалося отримати токен входу. Якщо проблема не зникає, спробуйте увійти через браузер з меню.
+ Авторизацію відхилено. Якщо ви впевнені, що вказали правильні облікові дані, спробуйте увійти через браузер з меню.
+ Сталася помилка невпізнання авторизації. Якщо проблема не зникає, спробуйте увійти через браузер з меню.
+ Помилка автентифікації цього сервера. Якщо проблема не зникає, спробуйте увійти через браузер з меню.
Введено недійсний домен
Показати поширення
Показати поширення
@@ -630,4 +630,16 @@
Вимкнено
<не налаштовано>
<недійсний>
+ Увійти через браузер
+ Працює в більшості випадків. Дані не витікають в інші застосунки.
+ Може підтримувати додаткові методи автентифікації, але для цього потрібен підтримуваний браузер.
+ Не вдалося вивантажити
+ Показати чернетки
+ Відхилити
+ Не вдалося вивантажити ваш допис і його було збережено в чернетках.
+\n
+\nАбо з не вдалося зв\'язатися з сервером, або він відхилив допис.
+ Не вдалося вивантажити ваш допис і його було збережено в чернетках.
+\n
+\nАбо з не вдалося зв\'язатися з сервером, або він відхилив допис.
\ No newline at end of file
diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml
index a696998e0..677b69026 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -93,7 +93,7 @@
Đăng lại URL tút với…
Đăng lại tút với…
Đang lưu vào thiết bị
- Tải về
+ Tải xuống
Đăng lại với tư cách …
Mở với tư cách %s
Chép URL
@@ -455,7 +455,7 @@
Bỏ ẩn %s
Ẩn tiêu đề tab
Đã lưu!
- Ghi chú
+ Ghi chú về người này
Chưa có thông báo.
Có gì mới\?
Ẩn số liệu trên trang hồ sơ
@@ -571,7 +571,7 @@
Hình ảnh phải có mô tả.
Không xác định được trạng thái máy chủ.
Cổng nên là khoảng giữa %d đến %d
- ✤
+ ✦
Hủy bỏ thay đổi
Tiếp tục sửa
Thay đổi chưa được lưu.
@@ -591,4 +591,16 @@
Tắt
<không đặt>
<không hợp lệ>
+ Có thể hỗ trợ các phương thức xác thực bổ sung nhưng yêu cầu trình duyệt được hỗ trợ.
+ Đăng nhập bằng trình duyệt
+ Đăng nhập ổn định. Dữ liệu sẽ không bị lộ.
+ Tải lên thất bại
+ Tút của bạn không tải lên được và đã được lưu nháp.
+\n
+\nKhông thể liên lạc được với máy chủ hoặc nó đã từ chối tút.
+ Tút của bạn không tải lên được và đã được lưu nháp.
+\n
+\nKhông thể liên lạc được với máy chủ hoặc nó đã từ chối tút.
+ Xem bản nháp
+ Bỏ qua
\ 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 51ef3bc77..2f75a2b72 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -4,11 +4,11 @@
网络请求出错,请检查互联网连接并重试!
内容不能为空。
该域名无效
- 未能通过该实例的身份验证。
+ 未能通过该实例的身份验证。如果这个问题持续,请从菜单处尝试“在浏览器中登录”。
找不到可用的浏览器。
- 发生不明授权错误。
- 授权被拒绝。
- 未能获取登录令牌。
+ 发生不明授权错误。如果这个问题持续,请从菜单处尝试 “在浏览器中登录”。
+ 授权被拒绝。如果你确定提供了正确的凭据,请从菜单处尝试“在浏览器中登录”。
+ 未能获取登录令牌。如果这个问题持续,请从菜单处尝试 “在浏览器中登录”。
嘟文太长了!
无法上传此类型的文件。
此文件无法打开。
@@ -60,9 +60,9 @@
取消喜欢
更多
发表嘟文
- 登录 Mastodon 帐号
+ 用 Tusky 登录
注销
- 确定要退出帐号 %1$s 吗?
+ 确定要退出账号 %1$s 吗?
关注
取消关注
屏蔽
@@ -78,7 +78,7 @@
关闭
个人资料
设置
- 帐户设置
+ 账户设置
喜欢
被隐藏的用户
被屏蔽的用户
@@ -141,9 +141,9 @@
标题
什么是实例?
正在连接…
- 请输入你帐号所在的 Mastodon 站点的域名,比如 mastodon.social,icosahedron.website,social.tchncs.de,等等 。
+ 请输入你账号所在的 Mastodon 站点的域名,比如 mastodon.social,icosahedron.website,social.tchncs.de,等等 。
\n
-\n还没有 Mastodon 帐号?你也可以输入想注册的 Mastodon 站点的域名,然后在该服务器创建新的帐号并授权 Yuito 登入。
+\n还没有 Mastodon 账号?你也可以输入想注册的 Mastodon 站点的域名,然后在该服务器创建新的账号并授权 Tusky 登入。
\n
\n在 Mastodon 里,你的账号信息储存在某一特定实例当中,但 Mastodon 可使跨站互动和站内互动一样简单。
\n
@@ -242,7 +242,7 @@
问题反馈:\n
https://github.com/accelforce/Yuito/issues
- Yuito 官方帐号
+ Yuito 官方账号
分享嘟文内容
分享嘟文链接
图片
@@ -271,8 +271,8 @@
移除
更新
需要过滤的文字
- 添加帐号
- 添加新的 Mastodon 帐号
+ 添加账号
+ 添加新的 Mastodon 账号
列表
列表
无法新建列表
@@ -293,7 +293,7 @@
设置图片标题
移除
- 保护你的帐户(锁嘟)
+ 保护你的账户(锁嘟)
你需要手动审核所有关注请求
保存为草稿?
正在发送嘟文…
@@ -425,8 +425,8 @@
转发到 %s
举报失败
无法获取嘟文
- 该报告将发送给给您的服务器管理员。您可以在下面提供有关回报此帐户的原因的说明:
- 该帐户来自其他服务器。向那里发送一份匿名的报告副本?
+ 该报告将发送给给您的服务器管理员。您可以在下面提供有关回报此账户的原因的说明:
+ 该账户来自其他服务器。向那里发送一份匿名的报告副本?
账户
搜索失败
显示通知过滤器
@@ -547,7 +547,7 @@
取关 #%s 出错
%s (%s)
(无更改)
- 帖子语言
+ 嘟文语言
%s (🔗 %s)
设置焦点失败
设置焦点
@@ -610,4 +610,16 @@
<无效>
从新到旧
从旧到新
+ 用浏览器登录
+ 多数情况下有效。没有数据泄露给其他应用。
+ 可能支持额外的验证方法但需要受支持的浏览器。
+ 你的嘟文上传失败,已被保存到草稿。
+\n
+\n要么是无法联系服务器,要么是服务器拒绝了它。
+ 显示草稿
+ 取消
+ 上传失败了
+ 你的嘟文上传失败,已被保存到草稿。
+\n
+\n要么是无法联系服务器,要么是服务器拒绝了它。
diff --git a/app/src/main/res/values-zh-rSG/strings.xml b/app/src/main/res/values-zh-rSG/strings.xml
index 0e173e006..2d87640b4 100644
--- a/app/src/main/res/values-zh-rSG/strings.xml
+++ b/app/src/main/res/values-zh-rSG/strings.xml
@@ -60,9 +60,9 @@
取消喜欢
更多
新嘟文
- 登录 Mastodon 帐号
+ 登录 Mastodon 账号
退出登录
- 确定要退出帐号 %1$s 吗?
+ 确定要退出账号 %1$s 吗?
关注
取消关注
屏蔽
@@ -141,10 +141,11 @@
标题
需要帮助?
正在连接…
- 请输入你帐号所在的 Mastodon 站点的域名,比如 pawoo.net,acg.mn,wxw.moe,等等 。
- \n\n还没有 Mastodon 帐号?你也可以输入想注册的 Mastodon 站点的域名,然后在该服务器创建新的帐号并授权 Yuito 登入。
- \n\n在 Mastodon 里,跨站互动和站内互动一样简单。可以前往 https://joinmastodon.org 了解更多信息。
-
+ 请输入你账号所在的 Mastodon 站点的域名,比如 pawoo.net,acg.mn,wxw.moe,等等 。
+\n
+\n还没有 Mastodon 账号?你也可以输入想注册的 Mastodon 站点的域名,然后在该服务器创建新的账号并授权 Tusky 登入。
+\n
+\n在 Mastodon 里,跨站互动和站内互动一样简单。可以前往 https://joinmastodon.org 了解更多信息。
正在结束上传…
正在上传…
下载
@@ -239,7 +240,7 @@
问题反馈:\n
https://github.com/accelforce/Yuito/issues
- Yuito 官方帐号
+ Yuito 官方账号
分享嘟文内容
分享嘟文链接
照片
@@ -268,8 +269,8 @@
移除
更新
需要过滤的文字
- 添加帐号
- 添加新的 Mastodon 帐号
+ 添加账号
+ 添加新的 Mastodon 账号
列表
列表
无法新建列表
diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml
new file mode 100644
index 000000000..c025d78a7
--- /dev/null
+++ b/app/src/main/res/values/plurals.xml
@@ -0,0 +1,6 @@
+
+
+ - @string/action_post_failed_detail
+ - @string/action_post_failed_detail_plural
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 758a8119a..8a2b664cc 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -4,11 +4,11 @@
A network error occurred! Please check your connection and try again!
This cannot be empty.
Invalid domain entered
- Failed authenticating with that instance.
+ Failed authenticating with that instance. If this persists, try "Login in Browser" from the menu.
Couldn\'t find a web browser to use.
- An unidentified authorization error occurred.
- Authorization was denied.
- Failed getting a login token.
+ An unidentified authorization error occurred. If this persists, try "Login in Browser" from the menu.
+ Authorization was denied. If you\'re sure that you supplied the correct credentials, try "Login in Browser" from the menu.
+ Failed getting a login token. If this persists, try "Login in Browser" from the menu.
Failed loading account details
Could not load the login page.
The post is too long!
@@ -101,9 +101,15 @@
Remove bookmark
More
Compose
- Log in with Mastodon
+ Login with Tusky
+ Login with Browser
Log out
Are you sure you want to log out of the account %1$s?
+ Upload failed
+ Your post failed to upload and has been saved to drafts.\n\nEither the server could not be contacted, or it rejected the post.
+ Your posts failed to upload and have been saved to drafts.\n\nEither the server could not be contacted, or it rejected the posts.
+ Show drafts
+ Dismiss
Follow
Unfollow
Block
@@ -593,6 +599,8 @@
Poll with choices: %1$s, %2$s, %3$s, %4$s; %5$s
Post language
+ Works in most cases. No data is leaked to other apps.
+ May support additional authentication methods, but requires a supported browser.
List name
diff --git a/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt
index 05da15075..97e2cf468 100644
--- a/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt
@@ -130,6 +130,7 @@ class MainActivityTest {
val viewModel = QuickTootViewModel(mockAccountManager, mock())
activity.eventHub = EventHub()
activity.accountManager = mockAccountManager
+ activity.draftsAlert = mock {}
activity.mastodonApi = mock {
onBlocking { accountVerifyCredentials() } doReturn NetworkResult.success(account)
onBlocking { listAnnouncements(false) } doReturn NetworkResult.success(emptyList())
diff --git a/fastlane/metadata/android/en-US/changelogs/100.txt b/fastlane/metadata/android/en-US/changelogs/100.txt
new file mode 100644
index 000000000..d2c450456
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/100.txt
@@ -0,0 +1,7 @@
+Tusky 21.0
+
+- Support for post editing
+- New setting to control preferred reading direction
+- Larger media previews and a new overlay to indicate media with description
+- It is now possible to add accounts to lists from their profile
+and much more
\ No newline at end of file
diff --git a/fastlane/metadata/android/eu/changelogs/87.txt b/fastlane/metadata/android/eu/changelogs/87.txt
new file mode 100644
index 000000000..fbe0eeaa7
--- /dev/null
+++ b/fastlane/metadata/android/eu/changelogs/87.txt
@@ -0,0 +1,8 @@
+Tusky 16.0 bertsioa
+
+- Denbora-lerroaren karga-logika guztiz berridatzia izan da, azkarragoa eta mantentzeko errazagoa izateko, baita akats gutxiago eduki dezan ere.
+- Orain, Tusky aplikazioak APNG eta animaziodun WebP formatudun emoji pertsonalizatuak anima ditzake.
+- Akats-zuzenketa ugari
+- Android 11rekin bateragarria
+- Itzulpen berriak: Eskoziako gaelera, galiziera eta ukrainera
+- Hobetutako itzulpenak
diff --git a/fastlane/metadata/android/eu/changelogs/97.txt b/fastlane/metadata/android/eu/changelogs/97.txt
new file mode 100644
index 000000000..ae439e0d8
--- /dev/null
+++ b/fastlane/metadata/android/eu/changelogs/97.txt
@@ -0,0 +1,9 @@
+Tusky 20.0
+
+- Aplikaziorako ikono berria Dzuk-en eskutik https://dzuk.zone
+- Orain traolak jarrai ditzakezu. Traolan klik egin eta ondoren, tresna-barrako ikonoa.
+- Android 13rekin bateragarria
+- Bidalketa bat egiterakoan, erabilitako hizkuntza aukera dezakezu
+- Profiletako edukien fitxak orain eduki hunkigarria errespetatzen du eta arinago kargatzen da.
+- Orain, irudiaren foku-puntua bidali aurretik zehazteko aukera duzu
+- Tresna-barran aukera berri bat zure erabiltzaile-izen osoa erakusteko
diff --git a/fastlane/metadata/android/uk/changelogs/100.txt b/fastlane/metadata/android/uk/changelogs/100.txt
new file mode 100644
index 000000000..845c0a6ec
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/100.txt
@@ -0,0 +1,7 @@
+Tusky 21.0
+
+- Підтримка редагування дописів
+- Нове налаштування для керування бажаним напрямком читання
+- Більші прев'ю медіа та новий вигляд позначення медіа з описом
+- З'явилася можливість додавати облікові записи до списків з їхнього профілю
+та багато іншого
diff --git a/fastlane/metadata/android/vi/changelogs/100.txt b/fastlane/metadata/android/vi/changelogs/100.txt
new file mode 100644
index 000000000..2a099320e
--- /dev/null
+++ b/fastlane/metadata/android/vi/changelogs/100.txt
@@ -0,0 +1,7 @@
+Tusky 21.0
+
+- Hỗ trợ sửa tút
+- Thêm cài đặt cách đọc
+- Xem trước media và lớp phủ mới để biểu thị phương tiện có mô tả
+- Cho phép thêm tài khoản vào danh sách hồ sơ
+và còn nữa
diff --git a/fastlane/metadata/android/zh-Hans/changelogs/100.txt b/fastlane/metadata/android/zh-Hans/changelogs/100.txt
new file mode 100644
index 000000000..bdd784c93
--- /dev/null
+++ b/fastlane/metadata/android/zh-Hans/changelogs/100.txt
@@ -0,0 +1,7 @@
+Tusky 21.0
+
+- 支持编辑嘟文
+- 控制偏首选阅读方向的新设置
+- 支持预览更大的媒体文件以及表明带描述媒体文件的新遮罩
+- 允许从账户的资料页将其添加到列表
+还有其他更多内容有待发现