diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/43.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/43.json
new file mode 100644
index 000000000..eeceb2031
--- /dev/null
+++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/43.json
@@ -0,0 +1,959 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 43,
+ "identityHash": "bf68abe55bb58765da7f9d6f7ef618e2",
+ "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, `scheduledAt` TEXT, `language` 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": "scheduledAt",
+ "columnName": "scheduledAt",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "language",
+ "columnName": "language",
+ "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, `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": "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, `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, 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": "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
+ }
+ ],
+ "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_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.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, 'bf68abe55bb58765da7f9d6f7ef618e2')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/44.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/44.json
new file mode 100644
index 000000000..e04b07549
--- /dev/null
+++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/44.json
@@ -0,0 +1,965 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 44,
+ "identityHash": "7b5271980102f35e55438f46777e3d46",
+ "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, `scheduledAt` TEXT, `language` 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": "scheduledAt",
+ "columnName": "scheduledAt",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "language",
+ "columnName": "language",
+ "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, `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, 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": "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
+ }
+ ],
+ "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_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.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, '7b5271980102f35e55438f46777e3d46')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/45.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/45.json
new file mode 100644
index 000000000..32d523da0
--- /dev/null
+++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/45.json
@@ -0,0 +1,983 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 45,
+ "identityHash": "edb371b819690636d843eebffa55792a",
+ "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, `scheduledAt` TEXT, `language` 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": "scheduledAt",
+ "columnName": "scheduledAt",
+ "affinity": "TEXT",
+ "notNull": false
+ },
+ {
+ "fieldPath": "language",
+ "columnName": "language",
+ "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, 'edb371b819690636d843eebffa55792a')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a36513400..000f4a5b4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -6,8 +6,8 @@
-
-
+
+
@@ -19,7 +19,8 @@
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/TuskyTheme"
- android:usesCleartextTraffic="false">
+ android:usesCleartextTraffic="false"
+ android:localeConfig="@xml/locales_config">
+
diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
index eaf2db8b9..cb262806a 100644
--- a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
+++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java
@@ -16,7 +16,6 @@
package com.keylesspalace.tusky;
import android.app.ActivityManager;
-import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
@@ -92,11 +91,6 @@ public abstract class BaseActivity extends AppCompatActivity implements Injectab
requesters = new HashMap<>();
}
- @Override
- protected void attachBaseContext(Context base) {
- super.attachBaseContext(TuskyApplication.getLocaleManager().setLocale(base));
- }
-
protected boolean requiresLogin() {
return true;
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt
index 628d91ab4..6f07a99f2 100644
--- a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt
@@ -29,10 +29,9 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.keylesspalace.tusky.components.account.AccountActivity
import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
import com.keylesspalace.tusky.network.MastodonApi
+import com.keylesspalace.tusky.util.looksLikeMastodonUrl
import com.keylesspalace.tusky.util.openLink
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
-import java.net.URI
-import java.net.URISyntaxException
import javax.inject.Inject
/** this is the base class for all activities that open links
@@ -180,47 +179,6 @@ abstract class BottomSheetActivity : BaseActivity() {
}
}
-// https://mastodon.foo.bar/@User
-// https://mastodon.foo.bar/@User/43456787654678
-// https://pleroma.foo.bar/users/User
-// https://pleroma.foo.bar/users/9qTHT2ANWUdXzENqC0
-// https://pleroma.foo.bar/notice/9sBHWIlwwGZi5QGlHc
-// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207
-// https://friendica.foo.bar/profile/user
-// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207
-// https://misskey.foo.bar/notes/83w6r388br (always lowercase)
-// https://pixelfed.social/p/connyduck/391263492998670833
-// https://pixelfed.social/connyduck
-// https://mastodon.foo.bar/users/User/statuses/000000000000000000
-fun looksLikeMastodonUrl(urlString: String): Boolean {
- val uri: URI
- try {
- uri = URI(urlString)
- } catch (e: URISyntaxException) {
- return false
- }
-
- if (uri.query != null ||
- uri.fragment != null ||
- uri.path == null
- ) {
- return false
- }
-
- val path = uri.path
- return path.matches("^/@[^/]+$".toRegex()) ||
- path.matches("^/@[^/]+/\\d+$".toRegex()) ||
- path.matches("^/users/\\w+$".toRegex()) ||
- path.matches("^/notice/[a-zA-Z0-9]+$".toRegex()) ||
- path.matches("^/objects/[-a-f0-9]+$".toRegex()) ||
- path.matches("^/notes/[a-z0-9]+$".toRegex()) ||
- path.matches("^/display/[-a-f0-9]+$".toRegex()) ||
- path.matches("^/profile/\\w+$".toRegex()) ||
- path.matches("^/p/\\w+/\\d+$".toRegex()) ||
- path.matches("^/\\w+$".toRegex()) ||
- path.matches("^/users/[^/]+/statuses/\\d+$".toRegex())
-}
-
enum class PostLookupFallbackBehavior {
OPEN_IN_BROWSER,
DISPLAY_ERROR,
diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
index 6c281a3c1..2fe21d231 100644
--- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt
@@ -434,9 +434,11 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
private fun setupDrawer(savedInstanceState: Bundle?, addSearchButton: Boolean) {
- binding.mainToolbar.setNavigationOnClickListener {
- binding.mainDrawerLayout.open()
- }
+ val drawerOpenClickListener = View.OnClickListener { binding.mainDrawerLayout.open() }
+
+ binding.mainToolbar.setNavigationOnClickListener(drawerOpenClickListener)
+ binding.topNavAvatar.setOnClickListener(drawerOpenClickListener)
+ binding.bottomNavAvatar.setOnClickListener(drawerOpenClickListener)
header = AccountHeaderView(this).apply {
headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP
@@ -648,7 +650,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val actionBarSize = ThemeUtils.getDimension(this, R.attr.actionBarSize)
val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin)
(binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin
- binding.tabLayout.hide()
+ binding.topNav.hide()
binding.bottomTabLayout
} else {
binding.bottomNav.hide()
@@ -784,7 +786,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
binding.viewPager.setPageTransformer(MarginPageTransformer(pageMargin))
- val enableSwipeForTabs = preferences.getBoolean("enableSwipeForTabs", true)
+ val enableSwipeForTabs = preferences.getBoolean(PrefKeys.ENABLE_SWIPE_FOR_TABS, true)
binding.viewPager.isUserInputEnabled = enableSwipeForTabs
onTabSelectedListener?.let {
@@ -922,71 +924,117 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje
}
private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) {
- val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
+ val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false)
val animateAvatars = preferences.getBoolean("animateGifAvatars", false)
- if (animateAvatars) {
- glide.asDrawable()
- .load(avatarUrl)
- .transform(
- RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
- )
- .apply {
- if (showPlaceholder) {
- placeholder(R.drawable.avatar_default)
- }
- }
- .into(object : CustomTarget(navIconSize, navIconSize) {
+ if (hideTopToolbar) {
+ val navOnBottom = preferences.getString("mainNavPosition", "top") == "bottom"
- override fun onLoadStarted(placeholder: Drawable?) {
- if (placeholder != null) {
- binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
- }
- }
+ val avatarView = if (navOnBottom) {
+ binding.bottomNavAvatar.show()
+ binding.bottomNavAvatar
+ } else {
+ binding.topNavAvatar.show()
+ binding.topNavAvatar
+ }
- override fun onResourceReady(resource: Drawable, transition: Transition?) {
- if (resource is Animatable) {
- resource.start()
- }
- binding.mainToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize)
- }
-
- override fun onLoadCleared(placeholder: Drawable?) {
- if (placeholder != null) {
- binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
- }
- }
- })
+ if (animateAvatars) {
+ Glide.with(this)
+ .load(avatarUrl)
+ .placeholder(R.drawable.avatar_default)
+ .into(avatarView)
+ } else {
+ Glide.with(this)
+ .asBitmap()
+ .load(avatarUrl)
+ .placeholder(R.drawable.avatar_default)
+ .into(avatarView)
+ }
} else {
- glide.asBitmap()
- .load(avatarUrl)
- .transform(
- RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
- )
- .apply {
- if (showPlaceholder) {
- placeholder(R.drawable.avatar_default)
- }
- }
- .into(object : CustomTarget(navIconSize, navIconSize) {
- override fun onLoadStarted(placeholder: Drawable?) {
- if (placeholder != null) {
- binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
+ binding.bottomNavAvatar.hide()
+ binding.topNavAvatar.hide()
+
+ val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size)
+
+ if (animateAvatars) {
+ glide.asDrawable()
+ .load(avatarUrl)
+ .transform(
+ RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
+ )
+ .apply {
+ if (showPlaceholder) {
+ placeholder(R.drawable.avatar_default)
}
}
+ .into(object : CustomTarget(navIconSize, navIconSize) {
- override fun onResourceReady(resource: Bitmap, transition: Transition?) {
- binding.mainToolbar.navigationIcon = FixedSizeDrawable(BitmapDrawable(resources, resource), navIconSize, navIconSize)
- }
+ override fun onLoadStarted(placeholder: Drawable?) {
+ if (placeholder != null) {
+ binding.mainToolbar.navigationIcon =
+ FixedSizeDrawable(placeholder, navIconSize, navIconSize)
+ }
+ }
- override fun onLoadCleared(placeholder: Drawable?) {
- if (placeholder != null) {
- binding.mainToolbar.navigationIcon = FixedSizeDrawable(placeholder, navIconSize, navIconSize)
+ override fun onResourceReady(
+ resource: Drawable,
+ transition: Transition?
+ ) {
+ if (resource is Animatable) {
+ resource.start()
+ }
+ binding.mainToolbar.navigationIcon =
+ FixedSizeDrawable(resource, navIconSize, navIconSize)
+ }
+
+ override fun onLoadCleared(placeholder: Drawable?) {
+ if (placeholder != null) {
+ binding.mainToolbar.navigationIcon =
+ FixedSizeDrawable(placeholder, navIconSize, navIconSize)
+ }
+ }
+ })
+ } else {
+ glide.asBitmap()
+ .load(avatarUrl)
+ .transform(
+ RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp))
+ )
+ .apply {
+ if (showPlaceholder) {
+ placeholder(R.drawable.avatar_default)
}
}
- })
+ .into(object : CustomTarget(navIconSize, navIconSize) {
+
+ override fun onLoadStarted(placeholder: Drawable?) {
+ if (placeholder != null) {
+ binding.mainToolbar.navigationIcon =
+ FixedSizeDrawable(placeholder, navIconSize, navIconSize)
+ }
+ }
+
+ override fun onResourceReady(
+ resource: Bitmap,
+ transition: Transition?
+ ) {
+ binding.mainToolbar.navigationIcon = FixedSizeDrawable(
+ BitmapDrawable(resources, resource),
+ navIconSize,
+ navIconSize
+ )
+ }
+
+ override fun onLoadCleared(placeholder: Drawable?) {
+ if (placeholder != null) {
+ binding.mainToolbar.navigationIcon =
+ FixedSizeDrawable(placeholder, navIconSize, navIconSize)
+ }
+ }
+ })
+ }
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt
index 8c3d826ef..0ff2f3997 100644
--- a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt
@@ -26,13 +26,16 @@ import androidx.fragment.app.commit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import at.connyduck.calladapter.networkresult.fold
-import autodispose2.androidx.lifecycle.autoDispose
+import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider
+import autodispose2.autoDispose
import com.google.android.material.snackbar.Snackbar
import com.keylesspalace.tusky.appstore.EventHub
+import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
import com.keylesspalace.tusky.components.timeline.TimelineFragment
import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.Kind
import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding
import com.keylesspalace.tusky.di.ViewModelFactory
+import com.keylesspalace.tusky.entity.Filter
import com.keylesspalace.tusky.util.viewBinding
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
@@ -52,13 +55,20 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
private val quickTootViewModel: QuickTootViewModel by viewModels { viewModelFactory }
+
private val binding: ActivityStatuslistBinding by viewBinding(ActivityStatuslistBinding::inflate)
private lateinit var kind: Kind
private var hashtag: String? = null
private var followTagItem: MenuItem? = null
private var unfollowTagItem: MenuItem? = null
+ private var muteTagItem: MenuItem? = null
+ private var unmuteTagItem: MenuItem? = null
+
+ /** The filter muting hashtag, null if unknown or hashtag is not filtered */
+ private var mutedFilter: Filter? = null
override fun onCreate(savedInstanceState: Bundle?) {
+ Log.d("StatusListActivity", "onCreate")
super.onCreate(savedInstanceState)
setContentView(binding.root)
@@ -96,7 +106,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
eventHub.events
.observeOn(AndroidSchedulers.mainThread())
- .autoDispose(this, Lifecycle.Event.ON_DESTROY)
+ .autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
.subscribe(binding.viewQuickToot::handleEvent)
binding.floatingBtn.setOnClickListener(binding.viewQuickToot::onFABClicked)
}
@@ -110,10 +120,15 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
menuInflater.inflate(R.menu.view_hashtag_toolbar, menu)
followTagItem = menu.findItem(R.id.action_follow_hashtag)
unfollowTagItem = menu.findItem(R.id.action_unfollow_hashtag)
+ muteTagItem = menu.findItem(R.id.action_mute_hashtag)
+ unmuteTagItem = menu.findItem(R.id.action_unmute_hashtag)
followTagItem?.isVisible = tagEntity.following == false
unfollowTagItem?.isVisible = tagEntity.following == true
followTagItem?.setOnMenuItemClickListener { followTag() }
unfollowTagItem?.setOnMenuItemClickListener { unfollowTag() }
+ muteTagItem?.setOnMenuItemClickListener { muteTag() }
+ unmuteTagItem?.setOnMenuItemClickListener { unmuteTag() }
+ updateMuteTagMenuItems()
},
{
Log.w(TAG, "Failed to query tag #$tag", it)
@@ -165,6 +180,85 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector {
return true
}
+ /**
+ * Determine if the current hashtag is muted, and update the UI state accordingly.
+ */
+ private fun updateMuteTagMenuItems() {
+ val tag = hashtag ?: return
+
+ muteTagItem?.isVisible = true
+ muteTagItem?.isEnabled = false
+ unmuteTagItem?.isVisible = false
+
+ mastodonApi.getFilters().observeOn(AndroidSchedulers.mainThread())
+ .autoDispose(AndroidLifecycleScopeProvider.from(this, Lifecycle.Event.ON_DESTROY))
+ .subscribe { filters ->
+ for (filter in filters) {
+ if ((tag == filter.phrase) and filter.context.contains(Filter.HOME)) {
+ Log.d(TAG, "Tag $hashtag is filtered")
+ muteTagItem?.isVisible = false
+ unmuteTagItem?.isVisible = true
+ mutedFilter = filter
+ return@subscribe
+ }
+ }
+
+ Log.d(TAG, "Tag $hashtag is not filtered")
+ mutedFilter = null
+ muteTagItem?.isEnabled = true
+ muteTagItem?.isVisible = true
+ muteTagItem?.isVisible = true
+ }
+ }
+
+ private fun muteTag(): Boolean {
+ val tag = hashtag ?: return true
+
+ lifecycleScope.launch {
+ mastodonApi.createFilter(
+ tag,
+ listOf(Filter.HOME),
+ irreversible = false,
+ wholeWord = true,
+ expiresInSeconds = null
+ ).fold(
+ { filter ->
+ mutedFilter = filter
+ muteTagItem?.isVisible = false
+ unmuteTagItem?.isVisible = true
+ eventHub.dispatch(PreferenceChangedEvent(filter.context[0]))
+ },
+ {
+ Snackbar.make(binding.root, getString(R.string.error_muting_hashtag_format, tag), Snackbar.LENGTH_SHORT).show()
+ Log.e(TAG, "Failed to mute #$tag", it)
+ }
+ )
+ }
+
+ return true
+ }
+
+ private fun unmuteTag(): Boolean {
+ val filter = mutedFilter ?: return true
+
+ lifecycleScope.launch {
+ mastodonApi.deleteFilter(filter.id).fold(
+ {
+ muteTagItem?.isVisible = true
+ unmuteTagItem?.isVisible = false
+ eventHub.dispatch(PreferenceChangedEvent(filter.context[0]))
+ mutedFilter = null
+ },
+ {
+ Snackbar.make(binding.root, getString(R.string.error_unmuting_hashtag_format, filter.phrase), Snackbar.LENGTH_SHORT).show()
+ Log.e(TAG, "Failed to unmute #${filter.phrase}", it)
+ }
+ )
+ }
+
+ return true
+ }
+
override fun androidInjector() = dispatchingAndroidInjector
companion object {
diff --git a/app/src/main/java/com/keylesspalace/tusky/TabData.kt b/app/src/main/java/com/keylesspalace/tusky/TabData.kt
index cd789f017..7ea063e1e 100644
--- a/app/src/main/java/com/keylesspalace/tusky/TabData.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/TabData.kt
@@ -108,6 +108,6 @@ fun defaultTabs(): List {
createTabDataFromId(HOME),
createTabDataFromId(NOTIFICATIONS),
createTabDataFromId(LOCAL),
- createTabDataFromId(FEDERATED)
+ createTabDataFromId(DIRECT)
)
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
index ded947a84..5401b5931 100644
--- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt
@@ -16,8 +16,6 @@
package com.keylesspalace.tusky
import android.app.Application
-import android.content.Context
-import android.content.res.Configuration
import android.util.Log
import androidx.preference.PreferenceManager
import androidx.work.WorkManager
@@ -44,6 +42,9 @@ class TuskyApplication : Application(), HasAndroidInjector {
@Inject
lateinit var notificationWorkerFactory: NotificationWorkerFactory
+ @Inject
+ lateinit var localeManager: LocaleManager
+
override fun onCreate() {
// Uncomment me to get StrictMode violation logs
// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
@@ -74,6 +75,8 @@ class TuskyApplication : Application(), HasAndroidInjector {
val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT)
ThemeUtils.setAppNightMode(theme)
+ localeManager.setLocale()
+
RxJavaPlugins.setErrorHandler {
Log.w("RxJava", "undeliverable exception", it)
}
@@ -86,20 +89,5 @@ class TuskyApplication : Application(), HasAndroidInjector {
)
}
- override fun attachBaseContext(base: Context) {
- localeManager = LocaleManager(base)
- super.attachBaseContext(localeManager.setLocale(base))
- }
-
- override fun onConfigurationChanged(newConfig: Configuration) {
- super.onConfigurationChanged(newConfig)
- localeManager.setLocale(this)
- }
-
override fun androidInjector() = androidInjector
-
- companion object {
- @JvmStatic
- lateinit var localeManager: LocaleManager
- }
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
index 2a96cb7fe..8c7dff594 100644
--- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt
@@ -27,6 +27,7 @@ import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
+import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.transition.Transition
@@ -51,6 +52,7 @@ import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity
import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.fragment.ViewImageFragment
+import com.keylesspalace.tusky.fragment.ViewVideoFragment
import com.keylesspalace.tusky.pager.ImagePagerAdapter
import com.keylesspalace.tusky.pager.SingleImagePagerAdapter
import com.keylesspalace.tusky.util.getTemporaryMediaFilename
@@ -67,7 +69,7 @@ import java.util.Locale
typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit
-class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener {
+class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener, ViewVideoFragment.VideoActionsListener {
private val binding by viewBinding(ActivityViewMediaBinding::inflate)
@@ -212,12 +214,20 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener
}
private fun requestDownloadMedia() {
- requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
- if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- downloadMedia()
- } else {
- showErrorDialog(binding.toolbar, R.string.error_media_download_permission, R.string.action_retry) { requestDownloadMedia() }
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)) { _, grantResults ->
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ downloadMedia()
+ } else {
+ showErrorDialog(
+ binding.toolbar,
+ R.string.error_media_download_permission,
+ R.string.action_retry
+ ) { requestDownloadMedia() }
+ }
}
+ } else {
+ downloadMedia()
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt
index dc9ec70dc..d3412e5b1 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt
@@ -17,6 +17,7 @@ package com.keylesspalace.tusky.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
+import androidx.appcompat.widget.TooltipCompat
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding
@@ -52,6 +53,7 @@ class EmojiAdapter(
}
emojiImageView.contentDescription = emoji.shortcode
+ TooltipCompat.setTooltipText(emojiImageView, emoji.shortcode)
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt
index fdde2f21b..ef5edd1d4 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt
@@ -22,6 +22,8 @@ import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.TextView
import com.keylesspalace.tusky.util.ThemeUtils
+import com.keylesspalace.tusky.util.getTuskyDisplayName
+import com.keylesspalace.tusky.util.modernLanguageCode
import java.util.Locale
class LocaleAdapter(context: Context, resource: Int, locales: List) : ArrayAdapter(context, resource, locales) {
@@ -29,15 +31,14 @@ class LocaleAdapter(context: Context, resource: Int, locales: List) : Ar
return (super.getView(position, convertView, parent) as TextView).apply {
setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary))
typeface = Typeface.DEFAULT_BOLD
- text = super.getItem(position)?.language?.uppercase()
+ text = super.getItem(position)?.modernLanguageCode?.uppercase()
}
}
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
return (super.getDropDownView(position, convertView, parent) as TextView).apply {
setTextColor(ThemeUtils.getColor(context, android.R.attr.textColorTertiary))
- val locale = super.getItem(position)
- text = "${locale?.displayLanguage} (${locale?.getDisplayLanguage(locale)})"
+ text = super.getItem(position)?.getTuskyDisplayName(context)
}
}
}
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 b75ac7bf7..47ef253c3 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
@@ -43,6 +43,7 @@ import androidx.recyclerview.widget.RecyclerView;
import com.bumptech.glide.Glide;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding;
+import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding;
import com.keylesspalace.tusky.databinding.ViewQuoteInlineBinding;
import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Notification;
@@ -84,7 +85,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_FOLLOW = 2;
private static final int VIEW_TYPE_FOLLOW_REQUEST = 3;
private static final int VIEW_TYPE_PLACEHOLDER = 4;
- private static final int VIEW_TYPE_UNKNOWN = 5;
+ private static final int VIEW_TYPE_REPORT = 5;
+ private static final int VIEW_TYPE_UNKNOWN = 6;
private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE};
private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0];
@@ -141,6 +143,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
.inflate(R.layout.item_status_placeholder, parent, false);
return new PlaceholderViewHolder(view);
}
+ case VIEW_TYPE_REPORT: {
+ ItemReportNotificationBinding binding = ItemReportNotificationBinding.inflate(inflater, parent, false);
+ return new ReportNotificationViewHolder(binding);
+ }
default:
case VIEW_TYPE_UNKNOWN: {
View view = new View(parent.getContext());
@@ -256,6 +262,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
}
break;
}
+ case VIEW_TYPE_REPORT: {
+ if (payloadForHolder == null) {
+ ReportNotificationViewHolder holder = (ReportNotificationViewHolder) viewHolder;
+ holder.setupWithReport(concreteNotification.getAccount(), concreteNotification.getReport(), statusDisplayOptions.animateAvatars(), statusDisplayOptions.animateEmojis());
+ holder.setupActionListener(notificationActionListener, concreteNotification.getReport().getTargetAccount().getId(), concreteNotification.getAccount().getId(), concreteNotification.getReport().getId());
+ }
+ }
default:
}
}
@@ -309,6 +322,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case FOLLOW_REQUEST: {
return VIEW_TYPE_FOLLOW_REQUEST;
}
+ case REPORT: {
+ return VIEW_TYPE_REPORT;
+ }
default: {
return VIEW_TYPE_UNKNOWN;
}
@@ -327,6 +343,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
void onViewStatusForNotificationId(String notificationId);
+ void onViewReport(String reportId);
+
void onExpandedChange(boolean expanded, int position);
/**
diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt
new file mode 100644
index 000000000..0155f4a44
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/ReportNotificationViewHolder.kt
@@ -0,0 +1,90 @@
+/* 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.adapter
+
+import android.content.Context
+import androidx.core.content.ContextCompat
+import androidx.recyclerview.widget.RecyclerView
+import at.connyduck.sparkbutton.helpers.Utils
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.adapter.NotificationsAdapter.NotificationActionListener
+import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding
+import com.keylesspalace.tusky.entity.Report
+import com.keylesspalace.tusky.entity.TimelineAccount
+import com.keylesspalace.tusky.util.TimestampUtils
+import com.keylesspalace.tusky.util.emojify
+import com.keylesspalace.tusky.util.loadAvatar
+import com.keylesspalace.tusky.util.unicodeWrap
+import java.util.Date
+
+class ReportNotificationViewHolder(
+ private val binding: ItemReportNotificationBinding,
+) : RecyclerView.ViewHolder(binding.root) {
+
+ fun setupWithReport(reporter: TimelineAccount, report: Report, animateAvatar: Boolean, animateEmojis: Boolean) {
+ val reporterName = reporter.name.unicodeWrap().emojify(reporter.emojis, itemView, animateEmojis)
+ val reporteeName = report.targetAccount.name.unicodeWrap().emojify(report.targetAccount.emojis, itemView, animateEmojis)
+ val icon = ContextCompat.getDrawable(itemView.context, R.drawable.ic_flag_24dp)
+
+ binding.notificationTopText.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null)
+ binding.notificationTopText.text = itemView.context.getString(R.string.notification_header_report_format, reporterName, reporteeName)
+ binding.notificationSummary.text = itemView.context.getString(R.string.notification_summary_report_format, TimestampUtils.getRelativeTimeSpanString(itemView.context, report.createdAt.time, Date().time), report.status_ids?.size ?: 0)
+ binding.notificationCategory.text = getTranslatedCategory(itemView.context, report.category)
+
+ // Fancy avatar inset
+ val padding = Utils.dpToPx(binding.notificationReporteeAvatar.context, 12)
+ binding.notificationReporteeAvatar.setPaddingRelative(0, 0, padding, padding)
+
+ loadAvatar(
+ report.targetAccount.avatar,
+ binding.notificationReporteeAvatar,
+ itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp),
+ animateAvatar,
+ )
+ loadAvatar(
+ reporter.avatar,
+ binding.notificationReporterAvatar,
+ itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_24dp),
+ animateAvatar,
+ )
+ }
+
+ fun setupActionListener(listener: NotificationActionListener, reporteeId: String, reporterId: String, reportId: String) {
+ binding.notificationReporteeAvatar.setOnClickListener {
+ val position = bindingAdapterPosition
+ if (position != RecyclerView.NO_POSITION) {
+ listener.onViewAccount(reporteeId)
+ }
+ }
+ binding.notificationReporterAvatar.setOnClickListener {
+ val position = bindingAdapterPosition
+ if (position != RecyclerView.NO_POSITION) {
+ listener.onViewAccount(reporterId)
+ }
+ }
+
+ itemView.setOnClickListener { listener.onViewReport(reportId) }
+ }
+
+ private fun getTranslatedCategory(context: Context, rawCategory: String): String {
+ return when (rawCategory) {
+ "violation" -> context.getString(R.string.report_category_violation)
+ "spam" -> context.getString(R.string.report_category_spam)
+ "other" -> context.getString(R.string.report_category_other)
+ else -> rawCategory
+ }
+ }
+}
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 9d77b7bae..f69e9fc9c 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java
@@ -1,5 +1,7 @@
package com.keylesspalace.tusky.adapter;
+import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
+
import android.content.Context;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
@@ -23,6 +25,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.ContextCompat;
import androidx.core.text.HtmlCompat;
+import androidx.core.view.ViewKt;
import androidx.recyclerview.widget.DefaultItemAnimator;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -54,6 +57,7 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.TimestampUtils;
import com.keylesspalace.tusky.view.MediaPreviewImageView;
+import com.keylesspalace.tusky.view.MediaPreviewLayout;
import com.keylesspalace.tusky.viewdata.PollOptionViewData;
import com.keylesspalace.tusky.viewdata.PollViewData;
import com.keylesspalace.tusky.viewdata.PollViewDataKt;
@@ -67,14 +71,13 @@ import at.connyduck.sparkbutton.SparkButton;
import at.connyduck.sparkbutton.helpers.Utils;
import kotlin.collections.CollectionsKt;
-import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription;
-
import net.accelf.yuito.QuoteInlineHelper;
public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
public static class Key {
public static final String KEY_CREATED = "created";
}
+
private TextView displayName;
private TextView username;
private ImageButton replyButton;
@@ -85,8 +88,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
private SparkButton bookmarkButton;
private ImageButton moreButton;
private ConstraintLayout mediaContainer;
- protected MediaPreviewImageView[] mediaPreviews;
- private ImageView[] mediaOverlays;
+ protected MediaPreviewLayout mediaPreview;
private TextView sensitiveMediaWarning;
private View sensitiveMediaShow;
protected TextView[] mediaLabels;
@@ -139,19 +141,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
mediaContainer = itemView.findViewById(R.id.status_media_preview_container);
mediaContainer.setClipToOutline(true);
+ mediaPreview = itemView.findViewById(R.id.status_media_preview);
- mediaPreviews = new MediaPreviewImageView[]{
- itemView.findViewById(R.id.status_media_preview_0),
- itemView.findViewById(R.id.status_media_preview_1),
- itemView.findViewById(R.id.status_media_preview_2),
- itemView.findViewById(R.id.status_media_preview_3)
- };
- mediaOverlays = new ImageView[]{
- itemView.findViewById(R.id.status_media_overlay_0),
- itemView.findViewById(R.id.status_media_overlay_1),
- itemView.findViewById(R.id.status_media_overlay_2),
- itemView.findViewById(R.id.status_media_overlay_3)
- };
sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning);
sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button);
mediaLabels = new TextView[]{
@@ -190,8 +181,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
mediaPreviewUnloaded = new ColorDrawable(ThemeUtils.getColor(itemView.getContext(), R.attr.colorBackgroundAccent));
}
- protected abstract int getMediaPreviewHeight(Context context);
-
protected void setDisplayName(String name, List customEmojis, StatusDisplayOptions statusDisplayOptions) {
CharSequence emojifiedName = CustomEmojiHelper.emojify(
name, customEmojis, displayName, statusDisplayOptions.animateEmojis()
@@ -327,19 +316,25 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
}
- protected void setCreatedAt(Date createdAt, StatusDisplayOptions statusDisplayOptions) {
+ protected void setCreatedAt(Date createdAt, Date editedAt, StatusDisplayOptions statusDisplayOptions) {
+ String timestampText;
if (statusDisplayOptions.useAbsoluteTime()) {
- timestampInfo.setText(absoluteTimeFormatter.format(createdAt, true));
+ timestampText = absoluteTimeFormatter.format(createdAt, true);
} else {
if (createdAt == null) {
- timestampInfo.setText("?m");
+ timestampText = "?m";
} else {
long then = createdAt.getTime();
long now = System.currentTimeMillis();
String readout = TimestampUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now);
- timestampInfo.setText(readout);
+ timestampText = readout;
}
}
+
+ if (editedAt != null) {
+ timestampText = timestampInfo.getContext().getString(R.string.post_timestamp_with_edited_indicator, timestampText);
+ }
+ timestampInfo.setText(timestampText);
}
private CharSequence getCreatedAtDescription(Date createdAt,
@@ -501,64 +496,51 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Drawable placeholder = blurhash != null ? decodeBlurHash(blurhash) : mediaPreviewUnloaded;
- if (TextUtils.isEmpty(previewUrl)) {
- imageView.removeFocalPoint();
-
- Glide.with(imageView)
- .load(placeholder)
- .centerInside()
- .into(imageView);
-
- } else {
- Focus focus = meta != null ? meta.getFocus() : null;
-
- if (focus != null) { // If there is a focal point for this attachment:
- imageView.setFocalPoint(focus);
-
- Glide.with(imageView)
- .load(previewUrl)
- .placeholder(placeholder)
- .centerInside()
- .addListener(imageView)
- .into(imageView);
- } else {
+ ViewKt.doOnLayout(imageView, view -> {
+ if (TextUtils.isEmpty(previewUrl)) {
imageView.removeFocalPoint();
Glide.with(imageView)
- .load(previewUrl)
- .placeholder(placeholder)
+ .load(placeholder)
.centerInside()
.into(imageView);
+
+ } else {
+ Focus focus = meta != null ? meta.getFocus() : null;
+
+ if (focus != null) { // If there is a focal point for this attachment:
+ imageView.setFocalPoint(focus);
+
+ Glide.with(imageView)
+ .load(previewUrl)
+ .placeholder(placeholder)
+ .centerInside()
+ .addListener(imageView)
+ .into(imageView);
+ } else {
+ imageView.removeFocalPoint();
+
+ Glide.with(imageView)
+ .load(previewUrl)
+ .placeholder(placeholder)
+ .centerInside()
+ .into(imageView);
+ }
}
- }
+ return null;
+ });
}
protected void setMediaPreviews(final List attachments, boolean sensitive,
final StatusActionListener listener, boolean showingContent,
boolean useBlurhash) {
- Context context = itemView.getContext();
- final int n = Math.min(attachments.size(), Status.MAX_MEDIA_ATTACHMENTS);
+ mediaPreview.setAspectRatios(AttachmentHelper.aspectRatios(attachments));
- final int mediaPreviewHeight = getMediaPreviewHeight(context);
-
- if (n <= 2) {
- mediaPreviews[0].getLayoutParams().height = mediaPreviewHeight * 2;
- mediaPreviews[1].getLayoutParams().height = mediaPreviewHeight * 2;
- } else {
- mediaPreviews[0].getLayoutParams().height = mediaPreviewHeight;
- mediaPreviews[1].getLayoutParams().height = mediaPreviewHeight;
- mediaPreviews[2].getLayoutParams().height = mediaPreviewHeight;
- mediaPreviews[3].getLayoutParams().height = mediaPreviewHeight;
- }
-
- for (int i = 0; i < n; i++) {
+ mediaPreview.forEachIndexed((i, imageView) -> {
Attachment attachment = attachments.get(i);
String previewUrl = attachment.getPreviewUrl();
String description = attachment.getDescription();
- MediaPreviewImageView imageView = mediaPreviews[i];
-
- imageView.setVisibility(View.VISIBLE);
if (TextUtils.isEmpty(description)) {
imageView.setContentDescription(imageView.getContext()
@@ -576,42 +558,38 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
final Attachment.Type type = attachment.getType();
if (showingContent && (type == Attachment.Type.VIDEO || type == Attachment.Type.GIFV)) {
- mediaOverlays[i].setVisibility(View.VISIBLE);
+ imageView.setForeground(ContextCompat.getDrawable(itemView.getContext(), R.drawable.play_indicator_overlay));
} else {
- mediaOverlays[i].setVisibility(View.GONE);
+ imageView.setForeground(null);
}
setAttachmentClickListener(imageView, listener, i, attachment, true);
- }
- if (sensitive) {
- sensitiveMediaWarning.setText(R.string.post_sensitive_media_title);
- } else {
- sensitiveMediaWarning.setText(R.string.post_media_hidden_title);
- }
-
- sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE);
- sensitiveMediaShow.setVisibility(showingContent ? View.VISIBLE : View.GONE);
- sensitiveMediaShow.setOnClickListener(v -> {
- if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
- listener.onContentHiddenChange(false, getBindingAdapterPosition());
+ if (sensitive) {
+ sensitiveMediaWarning.setText(R.string.post_sensitive_media_title);
+ } else {
+ sensitiveMediaWarning.setText(R.string.post_media_hidden_title);
}
- v.setVisibility(View.GONE);
- sensitiveMediaWarning.setVisibility(View.VISIBLE);
- });
- sensitiveMediaWarning.setOnClickListener(v -> {
- if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
- listener.onContentHiddenChange(true, getBindingAdapterPosition());
- }
- v.setVisibility(View.GONE);
- sensitiveMediaShow.setVisibility(View.VISIBLE);
- });
+ sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE);
+ sensitiveMediaShow.setVisibility(showingContent ? View.VISIBLE : View.GONE);
+ sensitiveMediaShow.setOnClickListener(v -> {
+ if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
+ listener.onContentHiddenChange(false, getBindingAdapterPosition());
+ }
+ v.setVisibility(View.GONE);
+ sensitiveMediaWarning.setVisibility(View.VISIBLE);
+ });
+ sensitiveMediaWarning.setOnClickListener(v -> {
+ if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) {
+ listener.onContentHiddenChange(true, getBindingAdapterPosition());
+ }
+ v.setVisibility(View.GONE);
+ sensitiveMediaShow.setVisibility(View.VISIBLE);
+ });
- // Hide any of the placeholder previews beyond the ones set.
- for (int i = n; i < Status.MAX_MEDIA_ATTACHMENTS; i++) {
- mediaPreviews[i].setVisibility(View.GONE);
- }
+ return null;
+ });
}
@DrawableRes
@@ -835,7 +813,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
Status actionable = status.getActionable();
setDisplayName(actionable.getAccount().getName(), actionable.getAccount().getEmojis(), statusDisplayOptions);
setUsername(status.getUsername());
- setCreatedAt(actionable.getCreatedAt(), statusDisplayOptions);
+ setCreatedAt(actionable.getCreatedAt(), actionable.getEditedAt(), statusDisplayOptions);
setStatusVisibility(actionable.getVisibility());
setIsReply(actionable.getInReplyToId() != null);
setReplyCount(actionable.getRepliesCount());
@@ -860,10 +838,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
} else {
setMediaLabel(attachments, sensitive, listener, status.isShowingContent());
// Hide all unused views.
- mediaPreviews[0].setVisibility(View.GONE);
- mediaPreviews[1].setVisibility(View.GONE);
- mediaPreviews[2].setVisibility(View.GONE);
- mediaPreviews[3].setVisibility(View.GONE);
+ mediaPreview.setVisibility(View.GONE);
hideSensitiveMediaWarning();
}
@@ -893,7 +868,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
if (payloads instanceof List)
for (Object item : (List>) payloads) {
if (Key.KEY_CREATED.equals(item)) {
- setCreatedAt(status.getActionable().getCreatedAt(), statusDisplayOptions);
+ setCreatedAt(status.getActionable().getCreatedAt(), status.getActionable().getEditedAt(), statusDisplayOptions);
}
}
@@ -919,6 +894,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
getContentWarningDescription(context, status),
(TextUtils.isEmpty(status.getSpoilerText()) || !actionable.getSensitive() || status.isExpanded() ? status.getContent() : ""),
getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions),
+ actionable.getEditedAt() != null ? context.getString(R.string.description_post_edited) : "",
getReblogDescription(context, status),
status.getUsername(),
actionable.getReblogged() ? context.getString(R.string.description_post_reblogged) : "",
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 398ec2ccf..35b2d7ac5 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java
@@ -1,6 +1,7 @@
package com.keylesspalace.tusky.adapter;
import android.content.Context;
+import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.widget.TextView;
@@ -18,7 +19,9 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.viewdata.StatusViewData;
import java.text.DateFormat;
+import java.util.ArrayList;
import java.util.Date;
+import java.util.List;
public class StatusDetailedViewHolder extends StatusBaseViewHolder {
private final TextView reblogs;
@@ -33,18 +36,17 @@ public class StatusDetailedViewHolder extends StatusBaseViewHolder {
}
@Override
- protected int getMediaPreviewHeight(Context context) {
- return context.getResources().getDimensionPixelSize(R.dimen.status_detail_media_preview_height);
- }
-
- @Override
- protected void setCreatedAt(Date createdAt, StatusDisplayOptions statusDisplayOptions) {
- if (createdAt == null) {
- timestampInfo.setText("");
- } else {
- DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT);
- timestampInfo.setText(dateFormat.format(createdAt));
+ protected void setCreatedAt(Date createdAt, Date editedAt, StatusDisplayOptions statusDisplayOptions) {
+ DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT);
+ Context context = timestampInfo.getContext();
+ List list = new ArrayList<>();
+ if (createdAt != null) {
+ list.add(dateFormat.format(createdAt));
}
+ if (editedAt != null) {
+ list.add(context.getString(R.string.post_edited, dateFormat.format(editedAt)));
+ }
+ timestampInfo.setText(TextUtils.join(context.getString(R.string.timestamp_joiner), list));
}
private void setReblogAndFavCount(int reblogCount, int favCount, StatusActionListener listener) {
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 93c475643..76d129170 100644
--- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java
+++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java
@@ -53,11 +53,6 @@ public class StatusViewHolder extends StatusBaseViewHolder {
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content);
}
- @Override
- protected int getMediaPreviewHeight(Context context) {
- return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height);
- }
-
@Override
public void setupWithStatus(@NonNull StatusViewData.Concrete status,
@NonNull final StatusActionListener listener,
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt
index 30ebf436e..254218fc8 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt
@@ -53,6 +53,7 @@ import com.keylesspalace.tusky.EditProfileActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.StatusListActivity
import com.keylesspalace.tusky.ViewMediaActivity
+import com.keylesspalace.tusky.components.account.list.ListsForAccountFragment
import com.keylesspalace.tusky.components.compose.ComposeActivity
import com.keylesspalace.tusky.components.report.ReportActivity
import com.keylesspalace.tusky.databinding.ActivityAccountBinding
@@ -103,6 +104,8 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
private lateinit var accountFieldAdapter: AccountFieldAdapter
+ private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
+
private var followState: FollowState = FollowState.NOT_FOLLOWING
private var blocking: Boolean = false
private var muting: Boolean = false
@@ -247,6 +250,9 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin)
binding.accountFragmentViewPager.setPageTransformer(MarginPageTransformer(pageMargin))
+ val enableSwipeForTabs = preferences.getBoolean(PrefKeys.ENABLE_SWIPE_FOR_TABS, true)
+ binding.accountFragmentViewPager.isUserInputEnabled = enableSwipeForTabs
+
binding.accountTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabReselected(tab: TabLayout.Tab?) {
tab?.position?.let { position ->
@@ -737,6 +743,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
menu.removeItem(R.id.action_report)
}
+ if (!viewModel.isSelf && followState != FollowState.FOLLOWING) {
+ menu.removeItem(R.id.action_add_or_remove_from_list)
+ }
+
return super.onCreateOptionsMenu(menu)
}
@@ -849,6 +859,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI
toggleMute()
return true
}
+ R.id.action_add_or_remove_from_list -> {
+ ListsForAccountFragment.newInstance(viewModel.accountId).show(supportFragmentManager, null)
+ return true
+ }
R.id.action_mute_domain -> {
toggleBlockDomain(domain)
return true
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountFragment.kt
new file mode 100644
index 000000000..874d4b9f2
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountFragment.kt
@@ -0,0 +1,210 @@
+/* Copyright 2022 kyori19
+ *
+ * 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.account.list
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.viewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.ListAdapter
+import com.google.android.material.snackbar.Snackbar
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.databinding.FragmentListsForAccountBinding
+import com.keylesspalace.tusky.databinding.ItemAddOrRemoveFromListBinding
+import com.keylesspalace.tusky.di.Injectable
+import com.keylesspalace.tusky.di.ViewModelFactory
+import com.keylesspalace.tusky.util.BindingHolder
+import com.keylesspalace.tusky.util.hide
+import com.keylesspalace.tusky.util.show
+import com.keylesspalace.tusky.util.viewBinding
+import com.keylesspalace.tusky.util.visible
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import java.io.IOException
+import javax.inject.Inject
+
+class ListsForAccountFragment : DialogFragment(), Injectable {
+
+ @Inject
+ lateinit var viewModelFactory: ViewModelFactory
+
+ private val viewModel: ListsForAccountViewModel by viewModels { viewModelFactory }
+ private val binding by viewBinding(FragmentListsForAccountBinding::bind)
+
+ private val adapter = Adapter()
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle)
+
+ viewModel.setup(requireArguments().getString(ARG_ACCOUNT_ID)!!)
+ }
+
+ override fun onStart() {
+ super.onStart()
+ dialog?.apply {
+ window?.setLayout(
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ LinearLayout.LayoutParams.MATCH_PARENT,
+ )
+ }
+ }
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ return inflater.inflate(R.layout.fragment_lists_for_account, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ binding.listsView.layoutManager = LinearLayoutManager(view.context)
+ binding.listsView.adapter = adapter
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.states.collectLatest { states ->
+ binding.progressBar.hide()
+ if (states.isEmpty()) {
+ binding.messageView.show()
+ binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.no_lists) {
+ load()
+ }
+ } else {
+ binding.listsView.show()
+ adapter.submitList(states)
+ }
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.loadError.collectLatest { error ->
+ binding.progressBar.hide()
+ binding.listsView.hide()
+ binding.messageView.apply {
+ show()
+
+ if (error is IOException) {
+ setup(R.drawable.elephant_offline, R.string.error_network) {
+ load()
+ }
+ } else {
+ setup(R.drawable.elephant_error, R.string.error_generic) {
+ load()
+ }
+ }
+ }
+ }
+ }
+
+ viewLifecycleOwner.lifecycleScope.launch {
+ viewModel.actionError.collectLatest { error ->
+ when (error.type) {
+ ActionError.Type.ADD -> {
+ Snackbar.make(binding.root, R.string.failed_to_add_to_list, Snackbar.LENGTH_LONG)
+ .setAction(R.string.action_retry) {
+ viewModel.addAccountToList(error.listId)
+ }
+ .show()
+ }
+ ActionError.Type.REMOVE -> {
+ Snackbar.make(binding.root, R.string.failed_to_remove_from_list, Snackbar.LENGTH_LONG)
+ .setAction(R.string.action_retry) {
+ viewModel.removeAccountFromList(error.listId)
+ }
+ .show()
+ }
+ }
+ }
+ }
+
+ binding.doneButton.setOnClickListener {
+ dismiss()
+ }
+
+ load()
+ }
+
+ private fun load() {
+ binding.progressBar.show()
+ binding.listsView.hide()
+ binding.messageView.hide()
+ viewModel.load()
+ }
+
+ private object Differ : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(
+ oldItem: AccountListState,
+ newItem: AccountListState
+ ): Boolean {
+ return oldItem.list.id == newItem.list.id
+ }
+
+ override fun areContentsTheSame(
+ oldItem: AccountListState,
+ newItem: AccountListState
+ ): Boolean {
+ return oldItem == newItem
+ }
+ }
+
+ inner class Adapter :
+ ListAdapter>(Differ) {
+ override fun onCreateViewHolder(
+ parent: ViewGroup,
+ viewType: Int,
+ ): BindingHolder {
+ val binding =
+ ItemAddOrRemoveFromListBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ return BindingHolder(binding)
+ }
+
+ override fun onBindViewHolder(holder: BindingHolder, position: Int) {
+ val item = getItem(position)
+ holder.binding.listNameView.text = item.list.title
+ holder.binding.addButton.apply {
+ visible(!item.includesAccount)
+ setOnClickListener {
+ viewModel.addAccountToList(item.list.id)
+ }
+ }
+ holder.binding.removeButton.apply {
+ visible(item.includesAccount)
+ setOnClickListener {
+ viewModel.removeAccountFromList(item.list.id)
+ }
+ }
+ }
+ }
+
+ companion object {
+ private const val ARG_ACCOUNT_ID = "accountId"
+
+ fun newInstance(accountId: String): ListsForAccountFragment {
+ val args = Bundle().apply {
+ putString(ARG_ACCOUNT_ID, accountId)
+ }
+ return ListsForAccountFragment().apply { arguments = args }
+ }
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt
new file mode 100644
index 000000000..b571390e5
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt
@@ -0,0 +1,137 @@
+/* Copyright 2022 kyori19
+ *
+ * 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.account.list
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import at.connyduck.calladapter.networkresult.getOrThrow
+import at.connyduck.calladapter.networkresult.onFailure
+import at.connyduck.calladapter.networkresult.onSuccess
+import at.connyduck.calladapter.networkresult.runCatching
+import com.keylesspalace.tusky.entity.MastoList
+import com.keylesspalace.tusky.network.MastodonApi
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+
+data class AccountListState(
+ val list: MastoList,
+ val includesAccount: Boolean,
+)
+
+data class ActionError(
+ val error: Throwable,
+ val type: Type,
+ val listId: String,
+) : Throwable(error) {
+ enum class Type {
+ ADD,
+ REMOVE,
+ }
+}
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class ListsForAccountViewModel @Inject constructor(
+ private val mastodonApi: MastodonApi,
+) : ViewModel() {
+
+ private lateinit var accountId: String
+
+ private val _states = MutableSharedFlow>(1)
+ val states: SharedFlow> = _states
+
+ private val _loadError = MutableSharedFlow(1)
+ val loadError: SharedFlow = _loadError
+
+ private val _actionError = MutableSharedFlow(1)
+ val actionError: SharedFlow = _actionError
+
+ fun setup(accountId: String) {
+ this.accountId = accountId
+ }
+
+ fun load() {
+ _loadError.resetReplayCache()
+ viewModelScope.launch {
+ runCatching {
+ val (all, includes) = listOf(
+ async { mastodonApi.getLists() },
+ async { mastodonApi.getListsIncludesAccount(accountId) },
+ ).awaitAll()
+
+ _states.emit(
+ all.getOrThrow().map { list ->
+ AccountListState(
+ list = list,
+ includesAccount = includes.getOrThrow().any { it.id == list.id },
+ )
+ }
+ )
+ }
+ .onFailure {
+ _loadError.emit(it)
+ }
+ }
+ }
+
+ fun addAccountToList(listId: String) {
+ _actionError.resetReplayCache()
+ viewModelScope.launch {
+ mastodonApi.addAccountToList(listId, listOf(accountId))
+ .onSuccess {
+ _states.emit(
+ _states.first().map { state ->
+ if (state.list.id == listId) {
+ state.copy(includesAccount = true)
+ } else {
+ state
+ }
+ }
+ )
+ }
+ .onFailure {
+ _actionError.emit(ActionError(it, ActionError.Type.ADD, listId))
+ }
+ }
+ }
+
+ fun removeAccountFromList(listId: String) {
+ _actionError.resetReplayCache()
+ viewModelScope.launch {
+ mastodonApi.deleteAccountFromList(listId, listOf(accountId))
+ .onSuccess {
+ _states.emit(
+ _states.first().map { state ->
+ if (state.list.id == listId) {
+ state.copy(includesAccount = false)
+ } else {
+ state
+ }
+ }
+ )
+ }
+ .onFailure {
+ _actionError.emit(ActionError(it, ActionError.Type.REMOVE, listId))
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt
index 69d651d51..457cda7b9 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt
@@ -16,7 +16,6 @@
package com.keylesspalace.tusky.components.account.media
import android.os.Bundle
-import android.util.Log
import android.view.View
import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat
@@ -39,7 +38,6 @@ import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.openLink
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
-import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.viewdata.AttachmentViewData
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@@ -107,21 +105,30 @@ class AccountMediaFragment :
}
adapter.addLoadStateListener { loadState ->
- binding.progressBar.visible(loadState.refresh == LoadState.Loading && adapter.itemCount == 0)
+ binding.statusView.hide()
+ binding.progressBar.hide()
- if (loadState.refresh is LoadState.Error) {
- binding.recyclerView.hide()
- binding.statusView.show()
- val errorState = loadState.refresh as LoadState.Error
- if (errorState.error is IOException) {
- binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { adapter.retry() }
- } else {
- binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { adapter.retry() }
+ if (adapter.itemCount == 0) {
+ when (loadState.refresh) {
+ is LoadState.NotLoading -> {
+ if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) {
+ binding.statusView.show()
+ binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null)
+ }
+ }
+ is LoadState.Error -> {
+ binding.statusView.show()
+
+ if ((loadState.refresh as LoadState.Error).error is IOException) {
+ binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network, null)
+ } else {
+ binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic, null)
+ }
+ }
+ is LoadState.Loading -> {
+ binding.progressBar.show()
+ }
}
- Log.w(TAG, "error loading account media", errorState.error)
- } else {
- binding.recyclerView.show()
- binding.statusView.hide()
}
}
}
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 fe57bdb69..154519ba8 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
@@ -92,10 +92,13 @@ import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.PickMediaFiles
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.afterTextChanged
+import com.keylesspalace.tusky.util.getInitialLanguage
+import com.keylesspalace.tusky.util.getLocaleList
import com.keylesspalace.tusky.util.getMediaSize
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.highlightSpans
import com.keylesspalace.tusky.util.loadAvatar
+import com.keylesspalace.tusky.util.modernLanguageCode
import com.keylesspalace.tusky.util.onTextChanged
import com.keylesspalace.tusky.util.show
import com.keylesspalace.tusky.util.viewBinding
@@ -269,7 +272,7 @@ class ComposeActivity :
binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt)
}
- setupLanguageSpinner(getInitialLanguage(composeOptions?.language))
+ setupLanguageSpinner(getInitialLanguage(composeOptions?.language, accountManager.activeAccount))
setupComposeField(preferences, viewModel.startingText)
setupDefaultTagViews(preferences)
setupContentWarningField(composeOptions?.contentWarning)
@@ -604,37 +607,19 @@ class ComposeActivity :
)
}
- private fun setupLanguageSpinner(initialLanguage: String?) {
- val locales = Locale.getAvailableLocales()
- .filter { it.country.isNullOrEmpty() && it.script.isNullOrEmpty() && it.variant.isNullOrEmpty() } // Only "base" languages, "en" but not "en_DK"
- var currentLocaleIndex = locales.indexOfFirst { it.language == initialLanguage }
- if (currentLocaleIndex < 0) {
- Log.e(TAG, "Error looking up language tag '$initialLanguage', falling back to english")
- currentLocaleIndex = locales.indexOfFirst { it.language == "en" }
- }
-
- val context = this
+ private fun setupLanguageSpinner(initialLanguage: String) {
binding.composePostLanguageButton.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
- viewModel.postLanguage = (parent.adapter.getItem(position) as Locale).language
+ viewModel.postLanguage = (parent.adapter.getItem(position) as Locale).modernLanguageCode
}
override fun onNothingSelected(parent: AdapterView<*>) {
- parent.setSelection(locales.indexOfFirst { it.language == getInitialLanguage() })
+ parent.setSelection(0)
}
}
binding.composePostLanguageButton.apply {
- adapter = LocaleAdapter(context, android.R.layout.simple_spinner_dropdown_item, locales)
- setSelection(currentLocaleIndex)
- }
- }
-
- private fun getInitialLanguage(language: String? = null): String {
- return if (language.isNullOrEmpty()) {
- // Setting the application ui preference sets the default locale
- Locale.getDefault().language
- } else {
- language
+ adapter = LocaleAdapter(context, android.R.layout.simple_spinner_dropdown_item, getLocaleList(initialLanguage))
+ setSelection(0)
}
}
@@ -874,7 +859,7 @@ class ComposeActivity :
// Wait until bottom sheet is not collapsed and show next screen after
if (newState == BottomSheetBehavior.STATE_COLLAPSED) {
addMediaBehavior.removeBottomSheetCallback(this)
- if (ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && ContextCompat.checkSelfPermission(this@ComposeActivity, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this@ComposeActivity,
arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE),
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt
index 949fe6bf1..190de74c2 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt
@@ -80,6 +80,7 @@ data class ConversationStatusEntity(
val account: ConversationAccountEntity,
val content: String,
val createdAt: Date,
+ val editedAt: Date?,
val emojis: List,
val favouritesCount: Int,
val repliesCount: Int,
@@ -109,6 +110,7 @@ data class ConversationStatusEntity(
content = content,
reblog = null,
createdAt = createdAt,
+ editedAt = editedAt,
emojis = emojis,
reblogsCount = 0,
favouritesCount = favouritesCount,
@@ -147,7 +149,11 @@ fun TimelineAccount.toEntity() =
emojis = emojis ?: emptyList()
)
-fun Status.toEntity() =
+fun Status.toEntity(
+ expanded: Boolean,
+ contentShowing: Boolean,
+ contentCollapsed: Boolean
+) =
ConversationStatusEntity(
id = id,
url = url,
@@ -156,6 +162,7 @@ fun Status.toEntity() =
account = account.toEntity(),
content = content,
createdAt = createdAt,
+ editedAt = editedAt,
emojis = emojis,
favouritesCount = favouritesCount,
repliesCount = repliesCount,
@@ -166,20 +173,30 @@ fun Status.toEntity() =
attachments = attachments,
mentions = mentions,
tags = tags,
- showingHiddenContent = false,
- expanded = false,
- collapsed = true,
+ showingHiddenContent = contentShowing,
+ expanded = expanded,
+ collapsed = contentCollapsed,
muted = muted ?: false,
poll = poll,
language = language,
)
-fun Conversation.toEntity(accountId: Long, order: Int) =
+fun Conversation.toEntity(
+ accountId: Long,
+ order: Int,
+ expanded: Boolean,
+ contentShowing: Boolean,
+ contentCollapsed: Boolean
+) =
ConversationEntity(
accountId = accountId,
id = id,
order = order,
accounts = accounts.map { it.toEntity() },
unread = unread,
- lastStatus = lastStatus!!.toEntity()
+ lastStatus = lastStatus!!.toEntity(
+ expanded = expanded,
+ contentShowing = contentShowing,
+ contentCollapsed = contentCollapsed
+ )
)
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt
index f9082e8a3..04e69d4c4 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt
@@ -71,6 +71,7 @@ fun StatusViewData.Concrete.toConversationStatusEntity(
account = status.account.toEntity(),
content = status.content,
createdAt = status.createdAt,
+ editedAt = status.editedAt,
emojis = status.emojis,
favouritesCount = status.favouritesCount,
repliesCount = status.repliesCount,
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 64801b6d5..22da6de27 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
@@ -68,11 +68,6 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
this.listener = listener;
}
- @Override
- protected int getMediaPreviewHeight(Context context) {
- return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height);
- }
-
void setupWithConversation(
@NonNull ConversationViewData conversation,
@Nullable Object payloads
@@ -88,7 +83,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions);
setUsername(account.getUsername());
- setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
+ setCreatedAt(status.getCreatedAt(), status.getEditedAt(), statusDisplayOptions);
setIsReply(status.getInReplyToId() != null);
setFavourited(status.getFavourited());
setBookmarked(status.getBookmarked());
@@ -108,10 +103,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
} else {
setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent());
// Hide all unused views.
- mediaPreviews[0].setVisibility(View.GONE);
- mediaPreviews[1].setVisibility(View.GONE);
- mediaPreviews[2].setVisibility(View.GONE);
- mediaPreviews[3].setVisibility(View.GONE);
+ mediaPreview.setVisibility(View.GONE);
hideSensitiveMediaWarning();
}
@@ -129,7 +121,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder {
if (payloads instanceof List) {
for (Object item : (List>) payloads) {
if (Key.KEY_CREATED.equals(item)) {
- setCreatedAt(status.getCreatedAt(), statusDisplayOptions);
+ setCreatedAt(status.getCreatedAt(), status.getEditedAt(), statusDisplayOptions);
}
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt
index 02a44f951..921d694b2 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt
@@ -5,6 +5,7 @@ import androidx.paging.LoadType
import androidx.paging.PagingState
import androidx.paging.RemoteMediator
import androidx.room.withTransaction
+import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.HttpHeaderLink
@@ -12,15 +13,17 @@ import retrofit2.HttpException
@OptIn(ExperimentalPagingApi::class)
class ConversationsRemoteMediator(
- private val accountId: Long,
private val api: MastodonApi,
- private val db: AppDatabase
+ private val db: AppDatabase,
+ accountManager: AccountManager,
) : RemoteMediator() {
private var nextKey: String? = null
private var order: Int = 0
+ private val activeAccount = accountManager.activeAccount!!
+
override suspend fun load(
loadType: LoadType,
state: PagingState
@@ -46,7 +49,7 @@ class ConversationsRemoteMediator(
db.withTransaction {
if (loadType == LoadType.REFRESH) {
- db.conversationDao().deleteForAccount(accountId)
+ db.conversationDao().deleteForAccount(activeAccount.id)
}
val linkHeader = conversationsResponse.headers()["Link"]
@@ -56,8 +59,19 @@ class ConversationsRemoteMediator(
db.conversationDao().insert(
conversations
.filterNot { it.lastStatus == null }
- .map {
- it.toEntity(accountId, order++)
+ .map { conversation ->
+
+ val expanded = activeAccount.alwaysOpenSpoiler
+ val contentShowing = activeAccount.alwaysShowSensitiveMedia || !conversation.lastStatus!!.sensitive
+ val contentCollapsed = true
+
+ conversation.toEntity(
+ accountId = activeAccount.id,
+ order = order++,
+ expanded = expanded,
+ contentShowing = contentShowing,
+ contentCollapsed = contentCollapsed
+ )
}
)
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt
index 735aa26c1..4e140b761 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt
@@ -27,6 +27,7 @@ import com.keylesspalace.tusky.db.AccountManager
import com.keylesspalace.tusky.db.AppDatabase
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.usecase.TimelineCases
+import com.keylesspalace.tusky.util.EmptyPagingSource
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx3.await
@@ -42,8 +43,15 @@ class ConversationsViewModel @Inject constructor(
@OptIn(ExperimentalPagingApi::class)
val conversationFlow = Pager(
config = PagingConfig(pageSize = 30),
- remoteMediator = ConversationsRemoteMediator(accountManager.activeAccount!!.id, api, database),
- pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) }
+ remoteMediator = ConversationsRemoteMediator(api, database, accountManager),
+ pagingSourceFactory = {
+ val activeAccount = accountManager.activeAccount
+ if (activeAccount == null) {
+ EmptyPagingSource()
+ } else {
+ database.conversationDao().conversationsForAccount(activeAccount.id)
+ }
+ }
)
.flow
.map { pagingData ->
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt
new file mode 100644
index 000000000..0b8e7a5c7
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt
@@ -0,0 +1,148 @@
+package com.keylesspalace.tusky.components.followedtags
+
+import android.os.Bundle
+import android.util.Log
+import androidx.activity.viewModels
+import androidx.lifecycle.lifecycleScope
+import androidx.paging.LoadState
+import androidx.recyclerview.widget.DividerItemDecoration
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.SimpleItemAnimator
+import at.connyduck.calladapter.networkresult.fold
+import com.google.android.material.snackbar.Snackbar
+import com.keylesspalace.tusky.BaseActivity
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.databinding.ActivityFollowedTagsBinding
+import com.keylesspalace.tusky.di.ViewModelFactory
+import com.keylesspalace.tusky.interfaces.HashtagActionListener
+import com.keylesspalace.tusky.network.MastodonApi
+import com.keylesspalace.tusky.util.hide
+import com.keylesspalace.tusky.util.show
+import com.keylesspalace.tusky.util.viewBinding
+import com.keylesspalace.tusky.util.visible
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.launch
+import java.io.IOException
+import javax.inject.Inject
+
+class FollowedTagsActivity : BaseActivity(), HashtagActionListener {
+ @Inject
+ lateinit var api: MastodonApi
+
+ @Inject
+ lateinit var viewModelFactory: ViewModelFactory
+
+ private val binding by viewBinding(ActivityFollowedTagsBinding::inflate)
+ private val viewModel: FollowedTagsViewModel by viewModels { viewModelFactory }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContentView(binding.root)
+ setSupportActionBar(binding.includedToolbar.toolbar)
+ supportActionBar?.run {
+ setTitle(R.string.title_followed_hashtags)
+ // Back button
+ setDisplayHomeAsUpEnabled(true)
+ setDisplayShowHomeEnabled(true)
+ }
+
+ setupAdapter().let { adapter ->
+ setupRecyclerView(adapter)
+
+ lifecycleScope.launch {
+ viewModel.pager.collectLatest { pagingData ->
+ adapter.submitData(pagingData)
+ }
+ }
+ }
+ }
+
+ private fun setupRecyclerView(adapter: FollowedTagsAdapter) {
+ binding.followedTagsView.adapter = adapter
+ binding.followedTagsView.setHasFixedSize(true)
+ binding.followedTagsView.layoutManager = LinearLayoutManager(this)
+ binding.followedTagsView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
+ (binding.followedTagsView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
+ }
+
+ private fun setupAdapter(): FollowedTagsAdapter {
+ return FollowedTagsAdapter(this, viewModel).apply {
+ addLoadStateListener { loadState ->
+ binding.followedTagsProgressBar.visible(loadState.refresh == LoadState.Loading && itemCount == 0)
+
+ if (loadState.refresh is LoadState.Error) {
+ binding.followedTagsView.hide()
+ binding.followedTagsMessageView.show()
+ val errorState = loadState.refresh as LoadState.Error
+ if (errorState.error is IOException) {
+ binding.followedTagsMessageView.setup(R.drawable.elephant_offline, R.string.error_network) { retry() }
+ } else {
+ binding.followedTagsMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { retry() }
+ }
+ Log.w(TAG, "error loading followed hashtags", errorState.error)
+ } else {
+ binding.followedTagsView.show()
+ binding.followedTagsMessageView.hide()
+ }
+ }
+ }
+ }
+
+ private fun follow(tagName: String, position: Int) {
+ lifecycleScope.launch {
+ api.followTag(tagName).fold(
+ {
+ viewModel.tags.add(position, it)
+ viewModel.currentSource?.invalidate()
+ },
+ {
+ Snackbar.make(
+ this@FollowedTagsActivity,
+ binding.followedTagsView,
+ getString(R.string.error_following_hashtag_format, tagName),
+ Snackbar.LENGTH_SHORT
+ )
+ .show()
+ }
+ )
+ }
+ }
+
+ override fun unfollow(tagName: String, position: Int) {
+ lifecycleScope.launch {
+ api.unfollowTag(tagName).fold(
+ {
+ viewModel.tags.removeAt(position)
+ Snackbar.make(
+ this@FollowedTagsActivity,
+ binding.followedTagsView,
+ getString(R.string.confirmation_hashtag_unfollowed, tagName),
+ Snackbar.LENGTH_LONG
+ )
+ .setAction(R.string.action_undo) {
+ follow(tagName, position)
+ }
+ .show()
+ viewModel.currentSource?.invalidate()
+ },
+ {
+ Snackbar.make(
+ this@FollowedTagsActivity,
+ binding.followedTagsView,
+ getString(
+ R.string.error_unfollowing_hashtag_format,
+ tagName
+ ),
+ Snackbar.LENGTH_SHORT
+ )
+ .show()
+ }
+ )
+ }
+ }
+
+ companion object {
+ const val TAG = "FollowedTagsActivity"
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsAdapter.kt
new file mode 100644
index 000000000..365900886
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsAdapter.kt
@@ -0,0 +1,38 @@
+package com.keylesspalace.tusky.components.followedtags
+
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.ImageButton
+import android.widget.TextView
+import androidx.paging.PagingDataAdapter
+import androidx.recyclerview.widget.DiffUtil
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.databinding.ItemFollowedHashtagBinding
+import com.keylesspalace.tusky.interfaces.HashtagActionListener
+import com.keylesspalace.tusky.util.BindingHolder
+
+class FollowedTagsAdapter(
+ private val actionListener: HashtagActionListener,
+ private val viewModel: FollowedTagsViewModel,
+) : PagingDataAdapter>(STRING_COMPARATOR) {
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder =
+ BindingHolder(ItemFollowedHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false))
+
+ override fun onBindViewHolder(holder: BindingHolder, position: Int) {
+ viewModel.tags[position].let { tag ->
+ holder.itemView.findViewById(R.id.followed_tag).text = tag.name
+ holder.itemView.findViewById(R.id.followed_tag_unfollow).setOnClickListener {
+ actionListener.unfollow(tag.name, position)
+ }
+ }
+ }
+
+ override fun getItemCount(): Int = viewModel.tags.size
+
+ companion object {
+ val STRING_COMPARATOR = object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: String, newItem: String): Boolean = oldItem == newItem
+ override fun areContentsTheSame(oldItem: String, newItem: String): Boolean = oldItem == newItem
+ }
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsPagingSource.kt
new file mode 100644
index 000000000..da5479c9b
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsPagingSource.kt
@@ -0,0 +1,16 @@
+package com.keylesspalace.tusky.components.followedtags
+
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+
+class FollowedTagsPagingSource(private val viewModel: FollowedTagsViewModel) : PagingSource() {
+ override fun getRefreshKey(state: PagingState): String? = null
+
+ override suspend fun load(params: LoadParams): LoadResult {
+ return if (params is LoadParams.Refresh) {
+ LoadResult.Page(viewModel.tags.map { it.name }, null, viewModel.nextKey)
+ } else {
+ LoadResult.Page(emptyList(), null, null)
+ }
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsRemoteMediator.kt
new file mode 100644
index 000000000..649ca583e
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsRemoteMediator.kt
@@ -0,0 +1,57 @@
+package com.keylesspalace.tusky.components.followedtags
+
+import androidx.paging.ExperimentalPagingApi
+import androidx.paging.LoadType
+import androidx.paging.PagingState
+import androidx.paging.RemoteMediator
+import com.keylesspalace.tusky.entity.HashTag
+import com.keylesspalace.tusky.network.MastodonApi
+import com.keylesspalace.tusky.util.HttpHeaderLink
+import retrofit2.HttpException
+import retrofit2.Response
+
+@OptIn(ExperimentalPagingApi::class)
+class FollowedTagsRemoteMediator(
+ private val api: MastodonApi,
+ private val viewModel: FollowedTagsViewModel,
+) : RemoteMediator() {
+ override suspend fun load(
+ loadType: LoadType,
+ state: PagingState
+ ): MediatorResult {
+ return try {
+ val response = request(loadType)
+ ?: return MediatorResult.Success(endOfPaginationReached = true)
+
+ return applyResponse(response)
+ } catch (e: Exception) {
+ MediatorResult.Error(e)
+ }
+ }
+
+ private suspend fun request(loadType: LoadType): Response>? {
+ return when (loadType) {
+ LoadType.PREPEND -> null
+ LoadType.APPEND -> api.followedTags(maxId = viewModel.nextKey)
+ LoadType.REFRESH -> {
+ viewModel.nextKey = null
+ viewModel.tags.clear()
+ api.followedTags()
+ }
+ }
+ }
+
+ private fun applyResponse(response: Response>): MediatorResult {
+ val tags = response.body()
+ if (!response.isSuccessful || tags == null) {
+ return MediatorResult.Error(HttpException(response))
+ }
+
+ val links = HttpHeaderLink.parse(response.headers()["Link"])
+ viewModel.nextKey = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter("max_id")
+ viewModel.tags.addAll(tags)
+ viewModel.currentSource?.invalidate()
+
+ return MediatorResult.Success(endOfPaginationReached = viewModel.nextKey == null)
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsViewModel.kt
new file mode 100644
index 000000000..efe5661a9
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsViewModel.kt
@@ -0,0 +1,33 @@
+package com.keylesspalace.tusky.components.followedtags
+
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import androidx.paging.ExperimentalPagingApi
+import androidx.paging.Pager
+import androidx.paging.PagingConfig
+import androidx.paging.cachedIn
+import com.keylesspalace.tusky.di.Injectable
+import com.keylesspalace.tusky.entity.HashTag
+import com.keylesspalace.tusky.network.MastodonApi
+import javax.inject.Inject
+
+class FollowedTagsViewModel @Inject constructor (
+ api: MastodonApi
+) : ViewModel(), Injectable {
+ val tags: MutableList = mutableListOf()
+ var nextKey: String? = null
+ var currentSource: FollowedTagsPagingSource? = null
+
+ @OptIn(ExperimentalPagingApi::class)
+ val pager = Pager(
+ config = PagingConfig(pageSize = 100),
+ remoteMediator = FollowedTagsRemoteMediator(api, this),
+ pagingSourceFactory = {
+ FollowedTagsPagingSource(
+ viewModel = this
+ ).also { source ->
+ currentSource = source
+ }
+ },
+ ).flow.cachedIn(viewModelScope)
+}
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 beac22aad..af70cce2e 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
@@ -120,6 +120,7 @@ public class NotificationHelper {
public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS";
public static final String CHANNEL_SIGN_UP = "CHANNEL_SIGN_UP";
public static final String CHANNEL_UPDATES = "CHANNEL_UPDATES";
+ public static final String CHANNEL_REPORT = "CHANNEL_REPORT";
/**
* WorkManager Tag
@@ -401,6 +402,7 @@ public class NotificationHelper {
CHANNEL_SUBSCRIPTIONS + account.getIdentifier(),
CHANNEL_SIGN_UP + account.getIdentifier(),
CHANNEL_UPDATES + account.getIdentifier(),
+ CHANNEL_REPORT + account.getIdentifier(),
};
int[] channelNames = {
R.string.notification_mention_name,
@@ -412,6 +414,7 @@ public class NotificationHelper {
R.string.notification_subscription_name,
R.string.notification_sign_up_name,
R.string.notification_update_name,
+ R.string.notification_report_name,
};
int[] channelDescriptions = {
R.string.notification_mention_descriptions,
@@ -423,6 +426,7 @@ public class NotificationHelper {
R.string.notification_subscription_description,
R.string.notification_sign_up_description,
R.string.notification_update_description,
+ R.string.notification_report_description,
};
List channels = new ArrayList<>(6);
@@ -469,7 +473,7 @@ public class NotificationHelper {
if (notificationManager.areNotificationsEnabled()) {
for (NotificationChannel channel : notificationManager.getNotificationChannels()) {
- if (channel.getImportance() > NotificationManager.IMPORTANCE_NONE) {
+ if (channel != null && channel.getImportance() > NotificationManager.IMPORTANCE_NONE) {
Log.d(TAG, "NotificationsEnabled");
return true;
}
@@ -542,7 +546,7 @@ public class NotificationHelper {
return false;
}
NotificationChannel channel = notificationManager.getNotificationChannel(channelId);
- return channel.getImportance() > NotificationManager.IMPORTANCE_NONE;
+ return channel != null && channel.getImportance() > NotificationManager.IMPORTANCE_NONE;
}
switch (type) {
@@ -564,6 +568,8 @@ public class NotificationHelper {
return account.getNotificationsSignUps();
case UPDATE:
return account.getNotificationsUpdates();
+ case REPORT:
+ return account.getNotificationsReports();
default:
return false;
}
@@ -593,6 +599,10 @@ public class NotificationHelper {
return CHANNEL_POLL + account.getIdentifier();
case SIGN_UP:
return CHANNEL_SIGN_UP + account.getIdentifier();
+ case UPDATE:
+ return CHANNEL_UPDATES + account.getIdentifier();
+ case REPORT:
+ return CHANNEL_REPORT + account.getIdentifier();
default:
return null;
}
@@ -678,6 +688,8 @@ public class NotificationHelper {
return String.format(context.getString(R.string.notification_sign_up_format), accountName);
case UPDATE:
return String.format(context.getString(R.string.notification_update_format), accountName);
+ case REPORT:
+ return context.getString(R.string.notification_report_format, account.getDomain());
}
return null;
}
@@ -715,6 +727,12 @@ public class NotificationHelper {
}
return builder.toString();
}
+ case REPORT:
+ return context.getString(
+ R.string.notification_header_report_format,
+ StringUtils.unicodeWrap(notification.getAccount().getName()),
+ StringUtils.unicodeWrap(notification.getReport().getTargetAccount().getName())
+ );
}
return null;
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt
index ff4380d32..d60210ba5 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt
@@ -30,6 +30,7 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.TabPreferenceActivity
import com.keylesspalace.tusky.appstore.EventHub
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent
+import com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
import com.keylesspalace.tusky.components.login.LoginActivity
import com.keylesspalace.tusky.components.notifications.currentAccountNeedsMigration
@@ -47,6 +48,10 @@ import com.keylesspalace.tusky.settings.preference
import com.keylesspalace.tusky.settings.preferenceCategory
import com.keylesspalace.tusky.settings.switchPreference
import com.keylesspalace.tusky.util.ThemeUtils
+import com.keylesspalace.tusky.util.getInitialLanguage
+import com.keylesspalace.tusky.util.getLocaleList
+import com.keylesspalace.tusky.util.getTuskyDisplayName
+import com.keylesspalace.tusky.util.makeIcon
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.colorInt
@@ -66,6 +71,8 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject
lateinit var eventHub: EventHub
+ private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
+
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
val context = requireContext()
makePreferenceScreen {
@@ -95,6 +102,20 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
}
}
+ preference {
+ setTitle(R.string.title_followed_hashtags)
+ setIcon(R.drawable.ic_hashtag)
+ setOnPreferenceClickListener {
+ val intent = Intent(context, FollowedTagsActivity::class.java)
+ activity?.startActivity(intent)
+ activity?.overridePendingTransition(
+ R.anim.slide_from_right,
+ R.anim.slide_to_left
+ )
+ true
+ }
+ }
+
preference {
setTitle(R.string.action_view_mutes)
setIcon(R.drawable.ic_mute_24dp)
@@ -173,6 +194,29 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
}
}
+ listPreference {
+ val locales = getLocaleList(getInitialLanguage(null, accountManager.activeAccount))
+ setTitle(R.string.pref_default_post_language)
+ // Explicitly add "System default" to the start of the list
+ entries = (
+ listOf(context.getString(R.string.system_default)) + locales.map {
+ it.getTuskyDisplayName(context)
+ }
+ ).toTypedArray()
+ entryValues = (listOf("") + locales.map { it.language }).toTypedArray()
+ key = PrefKeys.DEFAULT_POST_LANGUAGE
+ icon = makeIcon(requireContext(), GoogleMaterial.Icon.gmd_translate, iconSize)
+ value = accountManager.activeAccount?.defaultPostLanguage ?: ""
+ isPersistent = false // This will be entirely server-driven
+ setSummaryProvider { entry }
+
+ setOnPreferenceChangeListener { _, newValue ->
+ syncWithServer(language = (newValue as String))
+ eventHub.dispatch(PreferenceChangedEvent(key))
+ true
+ }
+ }
+
switchPreference {
setTitle(R.string.pref_default_media_sensitivity)
setIcon(R.drawable.ic_eye_24dp)
@@ -301,8 +345,8 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
}
}
- private fun syncWithServer(visibility: String? = null, sensitive: Boolean? = null) {
- mastodonApi.accountUpdateSource(visibility, sensitive)
+ private fun syncWithServer(visibility: String? = null, sensitive: Boolean? = null, language: String? = null) {
+ mastodonApi.accountUpdateSource(visibility, sensitive, language)
.enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
val account = response.body()
@@ -312,6 +356,7 @@ class AccountPreferencesFragment : PreferenceFragmentCompat(), Injectable {
it.defaultPostPrivacy = account.source?.privacy
?: Status.Visibility.PUBLIC
it.defaultMediaSensitivity = account.source?.sensitive ?: false
+ it.defaultPostLanguage = language ?: ""
accountManager.saveAccount(it)
}
} 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 6fdc1e8ab..b6eb43894 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
@@ -144,6 +144,17 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable {
true
}
}
+
+ switchPreference {
+ setTitle(R.string.pref_title_notification_filter_reports)
+ key = PrefKeys.NOTIFICATION_FILTER_REPORTS
+ isIconSpaceReserved = false
+ isChecked = activeAccount.notificationsReports
+ setOnPreferenceChangeListener { _, newValue ->
+ updateAccount { it.notificationsReports = 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 1054c5682..ca6f8f1dd 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
@@ -72,30 +72,17 @@ class PreferencesActivity :
setDisplayShowHomeEnabled(true)
}
- val fragmentTag = "preference_fragment_$EXTRA_PREFERENCE_TYPE"
+ val preferenceType = intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0)
+
+ val fragmentTag = "preference_fragment_$preferenceType"
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_post_tabs)
- TabFilterPreferencesFragment.newInstance()
- }
- PROXY_PREFERENCES -> {
- setTitle(R.string.pref_title_http_proxy_settings)
- ProxyPreferencesFragment.newInstance()
- }
+ ?: when (preferenceType) {
+ GENERAL_PREFERENCES -> PreferencesFragment.newInstance()
+ ACCOUNT_PREFERENCES -> AccountPreferencesFragment.newInstance()
+ NOTIFICATION_PREFERENCES -> NotificationPreferencesFragment.newInstance()
+ TAB_FILTER_PREFERENCES -> TabFilterPreferencesFragment.newInstance()
+ PROXY_PREFERENCES -> ProxyPreferencesFragment.newInstance()
else -> throw IllegalArgumentException("preferenceType not known")
}
@@ -103,6 +90,14 @@ class PreferencesActivity :
replace(R.id.fragment_container, fragment, fragmentTag)
}
+ when (preferenceType) {
+ GENERAL_PREFERENCES -> setTitle(R.string.action_view_preferences)
+ ACCOUNT_PREFERENCES -> setTitle(R.string.action_view_account_preferences)
+ NOTIFICATION_PREFERENCES -> setTitle(R.string.pref_title_edit_notification_settings)
+ TAB_FILTER_PREFERENCES -> setTitle(R.string.pref_title_post_tabs)
+ PROXY_PREFERENCES -> setTitle(R.string.pref_title_http_proxy_settings)
+ }
+
onBackPressedDispatcher.addCallback(this, restartActivitiesOnBackPressedCallback)
restartActivitiesOnBackPressedCallback.isEnabled = savedInstanceState?.getBoolean(EXTRA_RESTART_ON_BACK, false) ?: false
}
@@ -141,10 +136,6 @@ class PreferencesActivity :
"enableSwipeForTabs", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR, "viewPagerOffScreenLimit" -> {
restartActivitiesOnBackPressedCallback.isEnabled = true
}
- "language" -> {
- restartActivitiesOnBackPressedCallback.isEnabled = true
- this.restartCurrentActivity()
- }
}
eventHub.dispatch(PreferenceChangedEvent(key))
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 bfe4c1f64..7fadb4979 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
@@ -33,14 +33,13 @@ import com.keylesspalace.tusky.settings.makePreferenceScreen
import com.keylesspalace.tusky.settings.preference
import com.keylesspalace.tusky.settings.preferenceCategory
import com.keylesspalace.tusky.settings.switchPreference
-import com.keylesspalace.tusky.util.ThemeUtils
+import com.keylesspalace.tusky.util.LocaleManager
import com.keylesspalace.tusky.util.deserialize
import com.keylesspalace.tusky.util.getNonNullString
+import com.keylesspalace.tusky.util.makeIcon
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
-import com.mikepenz.iconics.utils.sizePx
import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference
import javax.inject.Inject
@@ -49,6 +48,9 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
@Inject
lateinit var accountManager: AccountManager
+ @Inject
+ lateinit var localeManager: LocaleManager
+
private val iconSize by lazy { resources.getDimensionPixelSize(R.dimen.preference_icon_size) }
private var httpProxyPref: Preference? = null
@@ -77,10 +79,11 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
setDefaultValue("default")
setEntries(R.array.language_entries)
setEntryValues(R.array.language_values)
- key = PrefKeys.LANGUAGE
+ key = PrefKeys.LANGUAGE + "_" // deliberately not the actual key, the real handling happens in LocaleManager
setSummaryProvider { entry }
setTitle(R.string.pref_title_language)
icon = makeIcon(GoogleMaterial.Icon.gmd_translate)
+ preferenceDataStore = localeManager
}
listPreference {
@@ -350,11 +353,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
}
private fun makeIcon(icon: GoogleMaterial.Icon): IconicsDrawable {
- val context = requireContext()
- return IconicsDrawable(context, icon).apply {
- sizePx = iconSize
- colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
- }
+ return makeIcon(requireContext(), icon, iconSize)
}
override fun onResume() {
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 3d60fcdf8..d5071a554 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
@@ -22,12 +22,14 @@ import android.os.Bundle
import android.view.Menu
import androidx.activity.viewModels
import androidx.appcompat.widget.SearchView
+import androidx.preference.PreferenceManager
import com.google.android.material.tabs.TabLayoutMediator
import com.keylesspalace.tusky.BottomSheetActivity
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.components.search.adapter.SearchPagerAdapter
import com.keylesspalace.tusky.databinding.ActivitySearchBinding
import com.keylesspalace.tusky.di.ViewModelFactory
+import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.viewBinding
import dagger.android.DispatchingAndroidInjector
import dagger.android.HasAndroidInjector
@@ -44,6 +46,8 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
private val binding by viewBinding(ActivitySearchBinding::inflate)
+ private val preferences by lazy { PreferenceManager.getDefaultSharedPreferences(this) }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
@@ -61,6 +65,9 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector {
binding.pages.adapter = SearchPagerAdapter(this)
binding.pages.offscreenPageLimit = 4
+ val enableSwipeForTabs = preferences.getBoolean(PrefKeys.ENABLE_SWIPE_FOR_TABS, true)
+ binding.pages.isUserInputEnabled = enableSwipeForTabs
+
TabLayoutMediator(binding.tabs, binding.pages) {
tab, position ->
tab.text = getPageTitle(position)
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 d2d73878f..e958f0944 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
@@ -23,6 +23,7 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
+import android.os.Build
import android.os.Environment
import android.util.Log
import android.view.View
@@ -438,13 +439,21 @@ class SearchStatusesFragment : SearchFragment(), Status
}
private fun requestDownloadAllMedia(status: Status) {
- val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
- (activity as BaseActivity).requestPermissions(permissions) { _, grantResults ->
- if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- downloadAllMedia(status)
- } else {
- Toast.makeText(context, R.string.error_media_download_permission, Toast.LENGTH_SHORT).show()
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ val permissions = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ (activity as BaseActivity).requestPermissions(permissions) { _, grantResults ->
+ if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ downloadAllMedia(status)
+ } else {
+ Toast.makeText(
+ context,
+ R.string.error_media_download_permission,
+ Toast.LENGTH_SHORT
+ ).show()
+ }
}
+ } else {
+ downloadAllMedia(status)
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt
index 01b111184..4ecb4d3d0 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt
@@ -77,6 +77,7 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
inReplyToAccountId = null,
content = null,
createdAt = 0L,
+ editedAt = 0L,
emojis = null,
reblogsCount = 0,
favouritesCount = 0,
@@ -121,6 +122,7 @@ fun Status.toEntity(
inReplyToAccountId = actionableStatus.inReplyToAccountId,
content = actionableStatus.content,
createdAt = actionableStatus.createdAt.time,
+ editedAt = actionableStatus.editedAt?.time,
emojis = actionableStatus.emojis.let(gson::toJson),
reblogsCount = actionableStatus.reblogsCount,
favouritesCount = actionableStatus.favouritesCount,
@@ -173,6 +175,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
reblog = null,
content = status.content.orEmpty(),
createdAt = Date(status.createdAt),
+ editedAt = status.editedAt?.let { Date(it) },
emojis = emojis,
reblogsCount = status.reblogsCount,
favouritesCount = status.favouritesCount,
@@ -205,6 +208,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
reblog = reblog,
content = "",
createdAt = Date(status.createdAt), // lie but whatever?
+ editedAt = null,
emojis = listOf(),
reblogsCount = 0,
favouritesCount = 0,
@@ -236,6 +240,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData {
reblog = null,
content = status.content.orEmpty(),
createdAt = Date(status.createdAt),
+ editedAt = status.editedAt?.let { Date(it) },
emojis = emojis,
reblogsCount = status.reblogsCount,
favouritesCount = status.favouritesCount,
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt
index 187e2ae28..20c5ea5cc 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt
@@ -44,6 +44,7 @@ import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.FilterModel
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.usecase.TimelineCases
+import com.keylesspalace.tusky.util.EmptyPagingSource
import com.keylesspalace.tusky.viewdata.StatusViewData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
@@ -89,7 +90,7 @@ class CachedTimelineViewModel @Inject constructor(
pagingSourceFactory = {
val activeAccount = accountManager.activeAccount
if (activeAccount == null) {
- EmptyTimelinePagingSource()
+ EmptyPagingSource()
} else {
db.timelineDao().getStatuses(activeAccount.id)
}.also { newPagingSource ->
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/EmptyTimelinePagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/EmptyTimelinePagingSource.kt
deleted file mode 100644
index 5fd13dfb0..000000000
--- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/EmptyTimelinePagingSource.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.keylesspalace.tusky.components.timeline.viewmodel
-
-import androidx.paging.PagingSource
-import androidx.paging.PagingState
-import com.keylesspalace.tusky.db.TimelineStatusWithAccount
-
-class EmptyTimelinePagingSource : PagingSource() {
- override fun getRefreshKey(state: PagingState): Int? = null
-
- override suspend fun load(params: LoadParams): LoadResult = LoadResult.Page(emptyList(), null, null)
-}
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt
index f50ed2912..76cf930f4 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt
@@ -171,6 +171,12 @@ class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener,
}
}
is ThreadUiState.Success -> {
+ if (uiState.statuses.none { viewData -> viewData.isDetailed }) {
+ // no detailed statuses available, e.g. because author is blocked
+ activity?.finish()
+ return@collect
+ }
+
adapter.submitList(uiState.statuses) {
if (viewModel.isInitialLoad) {
viewModel.isInitialLoad = false
diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt
index c109494cd..f4d7e14d5 100644
--- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt
@@ -262,7 +262,7 @@ class ViewThreadViewModel @Inject constructor(
updateSuccess { uiState ->
uiState.copy(
statuses = uiState.statuses.filter { viewData ->
- viewData.status.account.id == accountId
+ viewData.status.account.id != accountId
}
)
}
@@ -366,11 +366,12 @@ class ViewThreadViewModel @Inject constructor(
}
private fun Status.toViewData(detailed: Boolean = false): StatusViewData.Concrete {
+ val oldStatus = (_uiState.value as? ThreadUiState.Success)?.statuses?.find { it.id == this.id }
return toViewData(
- isShowingContent = alwaysShowSensitiveMedia || !actionableStatus.sensitive,
- isExpanded = alwaysOpenSpoiler,
- isCollapsed = !detailed,
- isDetailed = detailed
+ isShowingContent = oldStatus?.isShowingContent ?: (alwaysShowSensitiveMedia || !actionableStatus.sensitive),
+ isExpanded = oldStatus?.isExpanded ?: alwaysOpenSpoiler,
+ isCollapsed = oldStatus?.isCollapsed ?: !detailed,
+ isDetailed = oldStatus?.isDetailed ?: detailed
)
}
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 5ffc9021d..f20660083 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt
@@ -54,11 +54,13 @@ data class AccountEntity(
var notificationsSubscriptions: Boolean = true,
var notificationsSignUps: Boolean = true,
var notificationsUpdates: Boolean = true,
+ var notificationsReports: Boolean = true,
var notificationSound: Boolean = true,
var notificationVibration: Boolean = true,
var notificationLight: Boolean = true,
var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC,
var defaultMediaSensitivity: Boolean = false,
+ var defaultPostLanguage: String = "",
var alwaysShowSensitiveMedia: Boolean = false,
var alwaysOpenSpoiler: Boolean = false,
var mediaPreviewEnabled: 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 951cb9776..c4956b814 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt
@@ -154,6 +154,7 @@ class AccountManager @Inject constructor(db: AppDatabase) {
it.displayName = account.name
it.profilePictureUrl = account.avatar
it.defaultPostPrivacy = account.source?.privacy ?: Status.Visibility.PUBLIC
+ it.defaultPostLanguage = account.source?.language ?: ""
it.defaultMediaSensitivity = account.source?.sensitive ?: false
it.emojis = account.emojis ?: emptyList()
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 be169e49c..843e20b21 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 = 42)
+ }, version = 45)
public abstract class AppDatabase extends RoomDatabase {
public abstract AccountDao accountDao();
@@ -611,4 +611,26 @@ public abstract class AppDatabase extends RoomDatabase {
database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_language` TEXT");
}
};
+
+ public static final Migration MIGRATION_42_43 = new Migration(42, 43) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultPostLanguage` TEXT NOT NULL DEFAULT ''");
+ }
+ };
+
+ public static final Migration MIGRATION_43_44 = new Migration(43, 44) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsReports` INTEGER NOT NULL DEFAULT 1");
+ }
+ };
+
+ public static final Migration MIGRATION_44_45 = new Migration(44, 45) {
+ @Override
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
+ database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `editedAt` INTEGER");
+ database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_editedAt` INTEGER");
+ }
+ };
}
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 8a4ef18b0..9882c70d6 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt
@@ -33,7 +33,7 @@ abstract class TimelineDao {
@Query(
"""
SELECT s.serverId, s.url, s.timelineUserId,
-s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt,
+s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt, s.editedAt,
s.emojis, s.reblogsCount, s.favouritesCount, s.repliesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive,
s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId,
s.content, s.attachments, s.poll, s.card, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned, s.language,
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 6d4d8ede7..0d3ed1c26 100644
--- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt
@@ -58,6 +58,7 @@ data class TimelineStatusEntity(
val inReplyToAccountId: String?,
val content: String?,
val createdAt: Long,
+ val editedAt: Long?,
val emojis: String?,
val reblogsCount: Int,
val favouritesCount: Int,
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 76f76ba7f..18c03797a 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt
@@ -31,6 +31,7 @@ import com.keylesspalace.tusky.components.account.AccountActivity
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.followedtags.FollowedTagsActivity
import com.keylesspalace.tusky.components.instancemute.InstanceListActivity
import com.keylesspalace.tusky.components.login.LoginActivity
import com.keylesspalace.tusky.components.login.LoginWebViewActivity
@@ -104,6 +105,9 @@ abstract class ActivitiesModule {
@ContributesAndroidInjector
abstract fun contributesFiltersActivity(): FiltersActivity
+ @ContributesAndroidInjector
+ abstract fun contributesFollowedTagsActivity(): FollowedTagsActivity
+
@ContributesAndroidInjector(modules = [FragmentBuildersModule::class])
abstract fun contributesReportActivity(): ReportActivity
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 9028dc1c7..a8e34c55e 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
@@ -72,7 +72,8 @@ class AppModule {
AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35,
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_41_42, AppDatabase.MIGRATION_42_43, AppDatabase.MIGRATION_43_44,
+ AppDatabase.MIGRATION_44_45,
)
.build()
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt
index ff508a52b..bea0ba789 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt
@@ -16,6 +16,7 @@
package com.keylesspalace.tusky.di
import com.keylesspalace.tusky.AccountsInListFragment
+import com.keylesspalace.tusky.components.account.list.ListsForAccountFragment
import com.keylesspalace.tusky.components.account.media.AccountMediaFragment
import com.keylesspalace.tusky.components.conversation.ConversationsFragment
import com.keylesspalace.tusky.components.instancemute.fragment.InstanceListFragment
@@ -93,6 +94,9 @@ abstract class FragmentBuildersModule {
@ContributesAndroidInjector
abstract fun preferencesFragment(): PreferencesFragment
+ @ContributesAndroidInjector
+ abstract fun listsForAccountFragment(): ListsForAccountFragment
+
@ContributesAndroidInjector
abstract fun searchNotestockFragment(): SearchNotestockFragment
}
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 9e865c6f4..8e23d2d40 100644
--- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt
@@ -5,11 +5,13 @@ package com.keylesspalace.tusky.di
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.keylesspalace.tusky.components.account.AccountViewModel
+import com.keylesspalace.tusky.components.account.list.ListsForAccountViewModel
import com.keylesspalace.tusky.components.account.media.AccountMediaViewModel
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.followedtags.FollowedTagsViewModel
import com.keylesspalace.tusky.components.login.LoginWebViewViewModel
import com.keylesspalace.tusky.components.report.ReportViewModel
import com.keylesspalace.tusky.components.scheduled.ScheduledStatusViewModel
@@ -127,6 +129,16 @@ abstract class ViewModelModule {
@ViewModelKey(LoginWebViewViewModel::class)
internal abstract fun loginWebViewViewModel(viewModel: LoginWebViewViewModel): ViewModel
+ @Binds
+ @IntoMap
+ @ViewModelKey(FollowedTagsViewModel::class)
+ internal abstract fun followedTagsViewModel(viewModel: FollowedTagsViewModel): ViewModel
+
+ @Binds
+ @IntoMap
+ @ViewModelKey(ListsForAccountViewModel::class)
+ internal abstract fun listsForAccountViewModel(viewModel: ListsForAccountViewModel): ViewModel
+
@Binds
@IntoMap
@ViewModelKey(QuickTootViewModel::class)
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt
index 16ec4507f..49bcda6bd 100644
--- a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt
@@ -59,7 +59,8 @@ data class AccountSource(
val privacy: Status.Visibility?,
val sensitive: Boolean?,
val note: String?,
- val fields: List?
+ val fields: List?,
+ val language: String?,
)
data class Field(
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt
index 27fdc8be6..c1368325e 100644
--- a/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt
@@ -68,7 +68,9 @@ data class Attachment(
@Parcelize
data class MetaData(
val focus: Focus?,
- val duration: Float?
+ val duration: Float?,
+ val original: Size?,
+ val small: Size?,
) : Parcelable
/**
@@ -82,4 +84,14 @@ data class Attachment(
val x: Float,
val y: Float
) : Parcelable
+
+ /**
+ * The size of an image, used to specify the width/height.
+ */
+ @Parcelize
+ data class Size(
+ val width: Int,
+ val height: Int,
+ val aspect: Double
+ ) : Parcelable
}
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 f6e381502..b058c4c10 100644
--- a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt
@@ -25,7 +25,8 @@ data class Notification(
val type: Type,
val id: String,
val account: TimelineAccount,
- val status: Status?
+ val status: Status?,
+ val report: Report?,
) {
@JsonAdapter(NotificationTypeAdapter::class)
@@ -40,6 +41,7 @@ data class Notification(
STATUS("status"),
SIGN_UP("admin.sign_up"),
UPDATE("update"),
+ REPORT("admin.report"),
;
companion object {
@@ -52,7 +54,7 @@ data class Notification(
}
return UNKNOWN
}
- val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS, SIGN_UP, UPDATE)
+ val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS, SIGN_UP, UPDATE, REPORT)
}
override fun toString(): String {
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Report.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Report.kt
new file mode 100644
index 000000000..0330c102c
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Report.kt
@@ -0,0 +1,12 @@
+package com.keylesspalace.tusky.entity
+
+import com.google.gson.annotations.SerializedName
+import java.util.Date
+
+data class Report(
+ val id: String,
+ val category: String,
+ val status_ids: List?,
+ @SerializedName("created_at") val createdAt: Date,
+ @SerializedName("target_account") val targetAccount: TimelineAccount,
+)
diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt
index 5d6f5ff07..9b4ef6704 100644
--- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt
@@ -30,6 +30,7 @@ data class Status(
val reblog: Status?,
val content: String,
@SerializedName("created_at", alternate = ["published"]) val createdAt: Date,
+ @SerializedName("edited_at") val editedAt: Date?,
val emojis: List,
@SerializedName("reblogs_count") val reblogsCount: Int,
@SerializedName("favourites_count") val favouritesCount: Int,
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 87df99aa0..a7c145c31 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
@@ -31,10 +31,8 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
-import android.widget.Button;
import android.widget.ListView;
import android.widget.PopupWindow;
-import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -67,6 +65,7 @@ import com.keylesspalace.tusky.appstore.PinEvent;
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent;
import com.keylesspalace.tusky.appstore.QuickReplyEvent;
import com.keylesspalace.tusky.appstore.ReblogEvent;
+import com.keylesspalace.tusky.databinding.FragmentTimelineNotificationsBinding;
import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.di.Injectable;
@@ -82,13 +81,13 @@ import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.HttpHeaderLink;
+import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
import com.keylesspalace.tusky.util.ListUtils;
import com.keylesspalace.tusky.util.NotificationTypeConverterKt;
import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.ViewDataUtils;
-import com.keylesspalace.tusky.view.BackgroundMessageView;
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
import com.keylesspalace.tusky.viewdata.NotificationViewData;
@@ -160,16 +159,11 @@ public class NotificationsFragment extends SFragment implements
@Inject
EventHub eventHub;
- private SwipeRefreshLayout swipeRefreshLayout;
- private RecyclerView recyclerView;
- private ProgressBar progressBar;
- private BackgroundMessageView statusView;
- private AppBarLayout appBarOptions;
+ private FragmentTimelineNotificationsBinding binding;
private LinearLayoutManager layoutManager;
private EndlessOnScrollListener scrollListener;
private NotificationsAdapter adapter;
- private Button buttonFilter;
private boolean hideFab;
private boolean topLoading;
private boolean bottomLoading;
@@ -213,35 +207,29 @@ public class NotificationsFragment extends SFragment implements
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.fragment_timeline_notifications, container, false);
+ binding = FragmentTimelineNotificationsBinding.inflate(inflater, container, false);
@NonNull Context context = inflater.getContext(); // from inflater to silence warning
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(requireContext());
boolean showNotificationsFilterSetting = preferences.getBoolean("showNotificationsFilter", true);
- //Clear notifications on filter visibility change to force refresh
+ // Clear notifications on filter visibility change to force refresh
if (showNotificationsFilterSetting != showNotificationsFilter)
notifications.clear();
showNotificationsFilter = showNotificationsFilterSetting;
// Setup the SwipeRefreshLayout.
- swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout);
- recyclerView = rootView.findViewById(R.id.recyclerView);
- progressBar = rootView.findViewById(R.id.progressBar);
- statusView = rootView.findViewById(R.id.statusView);
- appBarOptions = rootView.findViewById(R.id.appBarOptions);
-
- swipeRefreshLayout.setOnRefreshListener(this);
- swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue);
+ binding.swipeRefreshLayout.setOnRefreshListener(this);
+ binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue);
loadNotificationsFilter();
// Setup the RecyclerView.
- recyclerView.setHasFixedSize(true);
+ binding.recyclerView.setHasFixedSize(true);
layoutManager = new LinearLayoutManager(context);
- recyclerView.setLayoutManager(layoutManager);
- recyclerView.setAccessibilityDelegateCompat(
- new ListStatusAccessibilityDelegate(recyclerView, this, (pos) -> {
+ binding.recyclerView.setLayoutManager(layoutManager);
+ binding.recyclerView.setAccessibilityDelegateCompat(
+ new ListStatusAccessibilityDelegate(binding.recyclerView, this, (pos) -> {
NotificationViewData notification = notifications.getPairedItemOrNull(pos);
// We support replies only for now
if (notification instanceof NotificationViewData.Concrete) {
@@ -251,7 +239,7 @@ public class NotificationsFragment extends SFragment implements
}
}));
- recyclerView.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL));
+ binding.recyclerView.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL));
StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions(
preferences.getBoolean("animateGifAvatars", false),
@@ -271,7 +259,7 @@ public class NotificationsFragment extends SFragment implements
dataSource, statusDisplayOptions, this, this, this);
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
- recyclerView.setAdapter(adapter);
+ binding.recyclerView.setAdapter(adapter);
topLoading = false;
bottomLoading = false;
@@ -279,43 +267,47 @@ public class NotificationsFragment extends SFragment implements
updateAdapter();
- Button buttonClear = rootView.findViewById(R.id.buttonClear);
- buttonClear.setOnClickListener(v -> confirmClearNotifications());
- buttonFilter = rootView.findViewById(R.id.buttonFilter);
- buttonFilter.setOnClickListener(v -> showFilterMenu());
+ binding.buttonClear.setOnClickListener(v -> confirmClearNotifications());
+ binding.buttonFilter.setOnClickListener(v -> showFilterMenu());
if (notifications.isEmpty()) {
- swipeRefreshLayout.setEnabled(false);
+ binding.swipeRefreshLayout.setEnabled(false);
sendFetchNotificationsRequest(null, null, FetchEnd.BOTTOM, -1);
} else {
- progressBar.setVisibility(View.GONE);
+ binding.progressBar.setVisibility(View.GONE);
}
- ((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
+ ((SimpleItemAnimator) binding.recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
updateFilterVisibility();
- return rootView;
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ binding = null;
}
private void updateFilterVisibility() {
CoordinatorLayout.LayoutParams params =
- (CoordinatorLayout.LayoutParams) swipeRefreshLayout.getLayoutParams();
+ (CoordinatorLayout.LayoutParams) binding.swipeRefreshLayout.getLayoutParams();
if (showNotificationsFilter && !showingError) {
- appBarOptions.setExpanded(true, false);
- appBarOptions.setVisibility(View.VISIBLE);
- //Set content behaviour to hide filter on scroll
+ binding.appBarOptions.setExpanded(true, false);
+ binding.appBarOptions.setVisibility(View.VISIBLE);
+ // Set content behaviour to hide filter on scroll
params.setBehavior(new AppBarLayout.ScrollingViewBehavior());
} else {
- appBarOptions.setExpanded(false, false);
- appBarOptions.setVisibility(View.GONE);
- //Clear behaviour to hide app bar
+ binding.appBarOptions.setExpanded(false, false);
+ binding.appBarOptions.setVisibility(View.GONE);
+ // Clear behaviour to hide app bar
params.setBehavior(null);
}
}
private void confirmClearNotifications() {
- new AlertDialog.Builder(getContext())
+ new AlertDialog.Builder(requireContext())
.setMessage(R.string.notification_clear_text)
.setPositiveButton(android.R.string.ok, (DialogInterface dia, int which) -> clearNotifications())
.setNegativeButton(android.R.string.cancel, null)
@@ -328,10 +320,10 @@ public class NotificationsFragment extends SFragment implements
Activity activity = getActivity();
if (activity == null) throw new AssertionError("Activity is null");
- /* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't
- * guaranteed to be set until then.
- * Use a modified scroll listener that both loads more notificationsEnabled as it goes, and hides
- * the compose button on down-scroll. */
+ // This is delayed until onActivityCreated solely because MainActivity.composeButton
+ // isn't guaranteed to be set until then.
+ // Use a modified scroll listener that both loads more notificationsEnabled as it
+ // goes, and hides the compose button on down-scroll.
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(activity);
hideFab = preferences.getBoolean("fabHide", false);
scrollListener = new EndlessOnScrollListener(layoutManager) {
@@ -345,9 +337,9 @@ public class NotificationsFragment extends SFragment implements
if (composeButton != null) {
if (hideFab) {
if (dy > 0 && composeButton.isShown()) {
- composeButton.hide(); // hides the button if we're scrolling down
+ composeButton.hide(); // Hides the button if we're scrolling down
} else if (dy < 0 && !composeButton.isShown()) {
- composeButton.show(); // shows it if we are scrolling up
+ composeButton.show(); // Shows it if we are scrolling up
}
} else if (!composeButton.isShown()) {
composeButton.show();
@@ -361,7 +353,7 @@ public class NotificationsFragment extends SFragment implements
}
};
- recyclerView.addOnScrollListener(scrollListener);
+ binding.recyclerView.addOnScrollListener(scrollListener);
eventHub.getEvents()
.observeOn(AndroidSchedulers.mainThread())
@@ -385,7 +377,7 @@ public class NotificationsFragment extends SFragment implements
@Override
public void onRefresh() {
- this.statusView.setVisibility(View.GONE);
+ binding.statusView.setVisibility(View.GONE);
this.showingError = false;
Either first = CollectionsKt.firstOrNull(this.notifications);
String topId;
@@ -526,7 +518,7 @@ public class NotificationsFragment extends SFragment implements
@Override
public void onLoadMore(int position) {
- //check bounds before accessing list,
+ // Check bounds before accessing list,
if (notifications.size() >= position && position > 0) {
Notification previous = notifications.get(position - 1).asRightOrNull();
Notification next = notifications.get(position + 1).asRightOrNull();
@@ -548,7 +540,6 @@ public class NotificationsFragment extends SFragment implements
@Override
public void onContentCollapsedChange(boolean isCollapsed, int position) {
updateViewDataAt(position, (vd) -> vd.copyWithCollapsed(isCollapsed));
- ;
}
private void updateStatus(String statusId, Function mapper) {
@@ -623,28 +614,28 @@ public class NotificationsFragment extends SFragment implements
}
private void clearNotifications() {
- //Cancel all ongoing requests
- swipeRefreshLayout.setRefreshing(false);
+ // Cancel all ongoing requests
+ binding.swipeRefreshLayout.setRefreshing(false);
resetNotificationsLoad();
- //Show friend elephant
- this.statusView.setVisibility(View.VISIBLE);
- this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null);
+ // Show friend elephant
+ binding.statusView.setVisibility(View.VISIBLE);
+ binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null);
updateFilterVisibility();
- //Update adapter
+ // Update adapter
updateAdapter();
- //Execute clear notifications request
+ // Execute clear notifications request
mastodonApi.clearNotifications()
.observeOn(AndroidSchedulers.mainThread())
.to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(
response -> {
- // nothing to do
+ // Nothing to do
},
throwable -> {
- //Reload notifications on failure
+ // Reload notifications on failure
fullyRefreshWithProgressBar(true);
});
}
@@ -654,10 +645,10 @@ public class NotificationsFragment extends SFragment implements
bottomLoading = false;
topLoading = false;
- //Disable load more
+ // Disable load more
bottomId = null;
- //Clear exists notifications
+ // Clear exists notifications
notifications.clear();
}
@@ -696,7 +687,7 @@ public class NotificationsFragment extends SFragment implements
window.setFocusable(true);
window.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
window.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
- window.showAsDropDown(buttonFilter);
+ window.showAsDropDown(binding.buttonFilter);
}
@@ -720,6 +711,8 @@ public class NotificationsFragment extends SFragment implements
return getString(R.string.notification_sign_up_name);
case UPDATE:
return getString(R.string.notification_update_name);
+ case REPORT:
+ return getString(R.string.notification_report_name);
default:
return "Unknown";
}
@@ -762,12 +755,12 @@ public class NotificationsFragment extends SFragment implements
}
@Override
- public void onViewTag(String tag) {
+ public void onViewTag(@NonNull String tag) {
super.viewTag(tag);
}
@Override
- public void onViewAccount(String id) {
+ public void onViewAccount(@NonNull String id) {
super.viewAccount(id);
}
@@ -809,10 +802,15 @@ public class NotificationsFragment extends SFragment implements
Log.w(TAG, "Didn't find a notification for ID: " + notificationId);
}
+ @Override
+ public void onViewReport(String reportId) {
+ LinkHelper.openLink(requireContext(), String.format("https://%s/admin/reports/%s", accountManager.getActiveAccount().getDomain(), reportId));
+ }
+
private void onPreferenceChanged(String key) {
switch (key) {
case "fabHide": {
- hideFab = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("fabHide", false);
+ hideFab = PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean("fabHide", false);
break;
}
case "mediaPreviewEnabled": {
@@ -825,7 +823,7 @@ public class NotificationsFragment extends SFragment implements
}
case "showNotificationsFilter": {
if (isAdded()) {
- showNotificationsFilter = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("showNotificationsFilter", true);
+ showNotificationsFilter = PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean("showNotificationsFilter", true);
updateFilterVisibility();
fullyRefreshWithProgressBar(true);
}
@@ -841,7 +839,7 @@ public class NotificationsFragment extends SFragment implements
}
private void removeAllByAccountId(String accountId) {
- // using iterator to safely remove items while iterating
+ // Using iterator to safely remove items while iterating
Iterator> iterator = notifications.iterator();
while (iterator.hasNext()) {
Either notification = iterator.next();
@@ -855,7 +853,7 @@ public class NotificationsFragment extends SFragment implements
private void onLoadMore() {
if (bottomId == null) {
- // already loaded everything
+ // Already loaded everything
return;
}
@@ -885,7 +883,7 @@ public class NotificationsFragment extends SFragment implements
private void jumpToTop() {
if (isAdded()) {
- appBarOptions.setExpanded(true, false);
+ binding.appBarOptions.setExpanded(true, false);
layoutManager.scrollToPosition(0);
scrollListener.reset();
}
@@ -893,8 +891,8 @@ public class NotificationsFragment extends SFragment implements
private void sendFetchNotificationsRequest(String fromId, String uptoId,
final FetchEnd fetchEnd, final int pos) {
- /* If there is a fetch already ongoing, record however many fetches are requested and
- * fulfill them after it's complete. */
+ // If there is a fetch already ongoing, record however many fetches are requested and
+ // fulfill them after it's complete.
if (fetchEnd == FetchEnd.TOP && topLoading) {
return;
}
@@ -970,18 +968,18 @@ public class NotificationsFragment extends SFragment implements
}
if (notifications.size() == 0 && adapter.getItemCount() == 0) {
- this.statusView.setVisibility(View.VISIBLE);
- this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null);
+ binding.statusView.setVisibility(View.VISIBLE);
+ binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null);
}
updateFilterVisibility();
- swipeRefreshLayout.setEnabled(true);
- swipeRefreshLayout.setRefreshing(false);
- progressBar.setVisibility(View.GONE);
+ binding.swipeRefreshLayout.setEnabled(true);
+ binding.swipeRefreshLayout.setRefreshing(false);
+ binding.progressBar.setVisibility(View.GONE);
}
private void onFetchNotificationsFailure(Throwable throwable, FetchEnd fetchEnd, int position) {
- swipeRefreshLayout.setRefreshing(false);
+ binding.swipeRefreshLayout.setRefreshing(false);
if (fetchEnd == FetchEnd.MIDDLE && !notifications.get(position).isRight()) {
Placeholder placeholder = notifications.get(position).asLeft();
NotificationViewData placeholderVD =
@@ -989,18 +987,18 @@ public class NotificationsFragment extends SFragment implements
notifications.setPairedItem(position, placeholderVD);
updateAdapter();
} else if (this.notifications.isEmpty()) {
- this.statusView.setVisibility(View.VISIBLE);
- swipeRefreshLayout.setEnabled(false);
+ binding.statusView.setVisibility(View.VISIBLE);
+ binding.swipeRefreshLayout.setEnabled(false);
this.showingError = true;
if (throwable instanceof IOException) {
- this.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> {
- this.progressBar.setVisibility(View.VISIBLE);
+ binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network, __ -> {
+ binding.progressBar.setVisibility(View.VISIBLE);
this.onRefresh();
return Unit.INSTANCE;
});
} else {
- this.statusView.setup(R.drawable.elephant_error, R.string.error_generic, __ -> {
- this.progressBar.setVisibility(View.VISIBLE);
+ binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic, __ -> {
+ binding.progressBar.setVisibility(View.VISIBLE);
this.onRefresh();
return Unit.INSTANCE;
});
@@ -1016,7 +1014,7 @@ public class NotificationsFragment extends SFragment implements
bottomLoading = false;
}
- progressBar.setVisibility(View.GONE);
+ binding.progressBar.setVisibility(View.GONE);
}
private void saveNewestNotificationId(List notifications) {
@@ -1053,8 +1051,8 @@ public class NotificationsFragment extends SFragment implements
notifications.addAll(liftedNew);
} else {
int index = notifications.indexOf(liftedNew.get(newNotifications.size() - 1));
- for (int i = 0; i < index; i++) {
- notifications.remove(0);
+ if (index > 0) {
+ notifications.subList(0, index).clear();
}
int newIndex = liftedNew.indexOf(notifications.get(0));
@@ -1078,7 +1076,7 @@ public class NotificationsFragment extends SFragment implements
int end = notifications.size();
List> liftedNew = liftNotificationList(newNotifications);
Either last = notifications.get(end - 1);
- if (last != null && liftedNew.indexOf(last) == -1) {
+ if (last != null && !liftedNew.contains(last)) {
notifications.addAll(liftedNew);
updateAdapter();
}
@@ -1116,8 +1114,8 @@ public class NotificationsFragment extends SFragment implements
private void fullyRefreshWithProgressBar(boolean isShow) {
resetNotificationsLoad();
if (isShow) {
- progressBar.setVisibility(View.VISIBLE);
- statusView.setVisibility(View.GONE);
+ binding.progressBar.setVisibility(View.VISIBLE);
+ binding.statusView.setVisibility(View.GONE);
}
updateAdapter();
sendFetchNotificationsRequest(null, null, FetchEnd.TOP, -1);
@@ -1156,7 +1154,7 @@ public class NotificationsFragment extends SFragment implements
// scroll up when new items at the top are loaded while being at the start
// https://github.com/tuskyapp/Tusky/pull/1905#issuecomment-677819724
if (position == 0 && context != null && adapter.getItemCount() != count) {
- recyclerView.scrollBy(0, Utils.dpToPx(context, -30));
+ binding.recyclerView.scrollBy(0, Utils.dpToPx(context, -30));
}
}
}
@@ -1211,7 +1209,7 @@ public class NotificationsFragment extends SFragment implements
@Override
public Object getChangePayload(@NonNull NotificationViewData oldItem, @NonNull NotificationViewData newItem) {
if (oldItem.deepEquals(newItem)) {
- //If items are equal - update timestamp only
+ // If items are equal - update timestamp only
return Collections.singletonList(StatusBaseViewHolder.Key.KEY_CREATED);
} else
// If items are different - update a whole view holder
@@ -1237,7 +1235,7 @@ public class NotificationsFragment extends SFragment implements
* Auto dispose observable on pause
*/
private void startUpdateTimestamp() {
- SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
+ SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(requireContext());
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
if (!useAbsoluteTime) {
Observable.interval(0, 1, TimeUnit.MINUTES)
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 59f40f890..b70ffceea 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
@@ -25,6 +25,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
+import android.os.Build;
import android.os.Environment;
import android.util.Log;
import android.view.Menu;
@@ -501,13 +502,17 @@ public abstract class SFragment extends Fragment implements Injectable {
}
private void requestDownloadAllMedia(Status status) {
- String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
- ((BaseActivity) getActivity()).requestPermissions(permissions, (permissions1, grantResults) -> {
- if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
- downloadAllMedia(status);
- } else {
- Toast.makeText(getContext(), R.string.error_media_download_permission, Toast.LENGTH_SHORT).show();
- }
- });
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE};
+ ((BaseActivity) getActivity()).requestPermissions(permissions, (permissions1, grantResults) -> {
+ if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+ downloadAllMedia(status);
+ } else {
+ Toast.makeText(getContext(), R.string.error_media_download_permission, Toast.LENGTH_SHORT).show();
+ }
+ });
+ } else {
+ downloadAllMedia(status);
+ }
}
}
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 214741a8e..28991c262 100644
--- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt
@@ -18,27 +18,36 @@ package com.keylesspalace.tusky.fragment
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.annotation.SuppressLint
+import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.text.method.ScrollingMovementMethod
+import android.view.GestureDetector
import android.view.KeyEvent
import android.view.LayoutInflater
+import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.MediaController
+import androidx.core.view.GestureDetectorCompat
import com.keylesspalace.tusky.ViewMediaActivity
import com.keylesspalace.tusky.databinding.FragmentViewVideoBinding
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.visible
import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView
+import kotlin.math.abs
class ViewVideoFragment : ViewMediaFragment() {
+ interface VideoActionsListener {
+ fun onDismiss()
+ }
private var _binding: FragmentViewVideoBinding? = null
private val binding get() = _binding!!
+ private lateinit var videoActionsListener: VideoActionsListener
private lateinit var toolbar: View
private val handler = Handler(Looper.getMainLooper())
private val hideToolbar = Runnable {
@@ -52,6 +61,11 @@ class ViewVideoFragment : ViewMediaFragment() {
private lateinit var mediaController: MediaController
private var isAudio = false
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+ videoActionsListener = context as VideoActionsListener
+ }
+
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
// Start/pause/resume video playback as fragment is shown/hidden
super.setUserVisibleHint(isVisibleToUser)
@@ -168,6 +182,7 @@ class ViewVideoFragment : ViewMediaFragment() {
return binding.root
}
+ @SuppressLint("ClickableViewAccessibility")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val attachment = arguments?.getParcelable(ARG_ATTACHMENT)
@@ -177,6 +192,54 @@ class ViewVideoFragment : ViewMediaFragment() {
}
val url = attachment.url
isAudio = attachment.type == Attachment.Type.AUDIO
+
+ val gestureDetector = GestureDetectorCompat(
+ requireContext(),
+ object : GestureDetector.SimpleOnGestureListener() {
+ override fun onDown(event: MotionEvent): Boolean {
+ return true
+ }
+
+ override fun onFling(
+ e1: MotionEvent,
+ e2: MotionEvent,
+ velocityX: Float,
+ velocityY: Float
+ ): Boolean {
+ if (abs(velocityY) > abs(velocityX)) {
+ videoActionsListener.onDismiss()
+ return true
+ }
+ return false
+ }
+ }
+ )
+
+ var lastY = 0f
+ binding.root.setOnTouchListener { _, event ->
+ if (event.action == MotionEvent.ACTION_DOWN) {
+ lastY = event.rawY
+ } else if (event.pointerCount == 1 && event.action == MotionEvent.ACTION_MOVE) {
+ val diff = event.rawY - lastY
+ if (binding.videoView.translationY != 0f || abs(diff) > 40) {
+ binding.videoView.translationY += diff
+ val scale = (-abs(binding.videoView.translationY) / 720 + 1).coerceAtLeast(0.5f)
+ binding.videoView.scaleY = scale
+ binding.videoView.scaleX = scale
+ lastY = event.rawY
+ return@setOnTouchListener true
+ }
+ } else if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
+ if (abs(binding.videoView.translationY) > 180) {
+ videoActionsListener.onDismiss()
+ } else {
+ binding.videoView.animate().translationY(0f).scaleX(1f).scaleY(1f).start()
+ }
+ }
+
+ gestureDetector.onTouchEvent(event)
+ }
+
finalizeViewSetup(url, attachment.previewUrl, attachment.description)
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/HashtagActionListener.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/HashtagActionListener.kt
new file mode 100644
index 000000000..a223f268d
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/HashtagActionListener.kt
@@ -0,0 +1,5 @@
+package com.keylesspalace.tusky.interfaces
+
+interface HashtagActionListener {
+ fun unfollow(tagName: String, position: Int)
+}
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 f92729941..42652a122 100644
--- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
@@ -267,7 +267,8 @@ interface MastodonApi {
@PATCH("api/v1/accounts/update_credentials")
fun accountUpdateSource(
@Field("source[privacy]") privacy: String?,
- @Field("source[sensitive]") sensitive: Boolean?
+ @Field("source[sensitive]") sensitive: Boolean?,
+ @Field("source[language]") language: String?,
): Call
@Multipart
@@ -481,6 +482,11 @@ interface MastodonApi {
@GET("/api/v1/lists")
suspend fun getLists(): NetworkResult>
+ @GET("/api/v1/accounts/{id}/lists")
+ suspend fun getListsIncludesAccount(
+ @Path("id") accountId: String
+ ): NetworkResult>
+
@FormUrlEncoded
@POST("api/v1/lists")
suspend fun createList(
@@ -668,6 +674,14 @@ interface MastodonApi {
@GET("api/v1/tags/{name}")
suspend fun tag(@Path("name") name: String): NetworkResult
+ @GET("api/v1/followed_tags")
+ suspend fun followedTags(
+ @Query("min_id") minId: String? = null,
+ @Query("since_id") sinceId: String? = null,
+ @Query("max_id") maxId: String? = null,
+ @Query("limit") limit: Int? = null,
+ ): Response>
+
@POST("api/v1/tags/{name}/follow")
suspend fun followTag(@Path("name") name: String): NetworkResult
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 f5c92f507..e0ab6b33f 100644
--- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt
@@ -55,6 +55,7 @@ object PrefKeys {
const val STACK_TRACE = "stackTrace"
const val DEFAULT_POST_PRIVACY = "defaultPostPrivacy"
+ const val DEFAULT_POST_LANGUAGE = "defaultPostLanguage"
const val DEFAULT_MEDIA_SENSITIVITY = "defaultMediaSensitivity"
const val MEDIA_PREVIEW_ENABLED = "mediaPreviewEnabled"
const val ALWAYS_SHOW_SENSITIVE_MEDIA = "alwaysShowSensitiveMedia"
@@ -72,6 +73,7 @@ object PrefKeys {
const val NOTIFICATION_FILTER_SUBSCRIPTIONS = "notificationFilterSubscriptions"
const val NOTIFICATION_FILTER_SIGN_UPS = "notificationFilterSignUps"
const val NOTIFICATION_FILTER_UPDATES = "notificationFilterUpdates"
+ const val NOTIFICATION_FILTER_REPORTS = "notificationFilterReports"
const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies_v2" // This was changed once to reset an unintentionally set default.
const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts"
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/AttachmentHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/AttachmentHelper.kt
index 6307e7211..f49b901a0 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/AttachmentHelper.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/util/AttachmentHelper.kt
@@ -1,4 +1,5 @@
@file:JvmName("AttachmentHelper")
+
package com.keylesspalace.tusky.util
import android.content.Context
@@ -24,3 +25,12 @@ private fun formatDuration(durationInSeconds: Double): String {
val hours = durationInSeconds.toInt() / 3600
return "%d:%02d:%02d".format(hours, minutes, seconds)
}
+
+fun List.aspectRatios(): List {
+ return map { attachment ->
+ // clamp ratio between 2:1 & 1:2, defaulting to 16:9
+ val size = (attachment.meta?.small ?: attachment.meta?.original) ?: return@map 1.7778
+ val aspect = if (size.aspect > 0) size.aspect else size.width.toDouble() / size.height
+ aspect.coerceIn(0.5, 2.0)
+ }
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/EmptyPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/util/EmptyPagingSource.kt
new file mode 100644
index 000000000..41a12aa3d
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/EmptyPagingSource.kt
@@ -0,0 +1,10 @@
+package com.keylesspalace.tusky.util
+
+import androidx.paging.PagingSource
+import androidx.paging.PagingState
+
+class EmptyPagingSource : PagingSource() {
+ override fun getRefreshKey(state: PagingState): Int? = null
+
+ override suspend fun load(params: LoadParams): LoadResult = LoadResult.Page(emptyList(), null, null)
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/IconUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/IconUtils.kt
new file mode 100644
index 000000000..59b0b15d7
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/IconUtils.kt
@@ -0,0 +1,31 @@
+/* Copyright 2022 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.util
+
+import android.content.Context
+import androidx.annotation.Px
+import com.keylesspalace.tusky.R
+import com.mikepenz.iconics.IconicsDrawable
+import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
+import com.mikepenz.iconics.utils.colorInt
+import com.mikepenz.iconics.utils.sizePx
+
+fun makeIcon(context: Context, icon: GoogleMaterial.Icon, @Px iconSize: Int): IconicsDrawable {
+ return IconicsDrawable(context, icon).apply {
+ sizePx = iconSize
+ colorInt = ThemeUtils.getColor(context, R.attr.iconColor)
+ }
+}
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 1a7de9050..c385766e7 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt
@@ -37,6 +37,8 @@ import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.HashTag
import com.keylesspalace.tusky.entity.Status.Mention
import com.keylesspalace.tusky.interfaces.LinkListener
+import java.net.URI
+import java.net.URISyntaxException
fun getDomain(urlString: String?): String {
val host = urlString?.toUri()?.host
@@ -72,22 +74,16 @@ fun markupHiddenUrls(context: Context, content: CharSequence): SpannableStringBu
val spannableContent = SpannableStringBuilder(content)
val originalSpans = spannableContent.getSpans(0, content.length, URLSpan::class.java)
val obscuredLinkSpans = originalSpans.filter {
- val text = spannableContent.subSequence(spannableContent.getSpanStart(it), spannableContent.getSpanEnd(it))
- val firstCharacter = text[0]
+ val start = spannableContent.getSpanStart(it)
+ val firstCharacter = content[start]
return@filter if (firstCharacter == '#' || firstCharacter == '@') {
false
} else {
- val lastPart = text.toString().split(' ').lastOrNull() ?: ""
- var textDomain = getDomain(lastPart)
+ val text = spannableContent.subSequence(start, spannableContent.getSpanEnd(it)).toString()
+ .split(' ').lastOrNull() ?: ""
+ var textDomain = getDomain(text)
if (textDomain.isBlank()) {
- // Allow "some.domain" or "www.some.domain" without a domain notifier
- textDomain = lastPart
- if (textDomain.startsWith("www.")) {
- textDomain = textDomain.substring(4)
- }
- if (textDomain.endsWith("/")) {
- textDomain = textDomain.substring(0, textDomain.length - 1)
- }
+ textDomain = getDomain("https://$text")
}
getDomain(it.url) != textDomain
}
@@ -276,4 +272,49 @@ private fun openLinkInCustomTab(uri: Uri, context: Context) {
}
}
+// https://mastodon.foo.bar/@User
+// https://mastodon.foo.bar/@User/43456787654678
+// https://pleroma.foo.bar/users/User
+// https://pleroma.foo.bar/users/9qTHT2ANWUdXzENqC0
+// https://pleroma.foo.bar/notice/9sBHWIlwwGZi5QGlHc
+// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207
+// https://friendica.foo.bar/profile/user
+// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207
+// https://misskey.foo.bar/notes/83w6r388br (always lowercase)
+// https://pixelfed.social/p/connyduck/391263492998670833
+// https://pixelfed.social/connyduck
+// https://gts.foo.bar/@goblin/statuses/01GH9XANCJ0TA8Y95VE9H3Y0Q2
+// https://gts.foo.bar/@goblin
+// https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5
+fun looksLikeMastodonUrl(urlString: String): Boolean {
+ val uri: URI
+ try {
+ uri = URI(urlString)
+ } catch (e: URISyntaxException) {
+ return false
+ }
+
+ if (uri.query != null ||
+ uri.fragment != null ||
+ uri.path == null
+ ) {
+ return false
+ }
+
+ return uri.path.let {
+ it.matches("^/@[^/]+$".toRegex()) ||
+ it.matches("^/@[^/]+/\\d+$".toRegex()) ||
+ it.matches("^/users/\\w+$".toRegex()) ||
+ it.matches("^/notice/[a-zA-Z0-9]+$".toRegex()) ||
+ it.matches("^/objects/[-a-f0-9]+$".toRegex()) ||
+ it.matches("^/notes/[a-z0-9]+$".toRegex()) ||
+ it.matches("^/display/[-a-f0-9]+$".toRegex()) ||
+ it.matches("^/profile/\\w+$".toRegex()) ||
+ it.matches("^/p/\\w+/\\d+$".toRegex()) ||
+ it.matches("^/\\w+$".toRegex()) ||
+ it.matches("^/@[^/]+/statuses/[a-zA-Z0-9]+$".toRegex()) ||
+ it.matches("^/o/[a-f0-9]+$".toRegex())
+ }
+}
+
private const val TAG = "LinkHelper"
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LocaleExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/LocaleExtensions.kt
new file mode 100644
index 000000000..caab21927
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/LocaleExtensions.kt
@@ -0,0 +1,36 @@
+/* Copyright 2022 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.util
+
+import android.content.Context
+import com.keylesspalace.tusky.R
+import java.util.Locale
+
+// When a language code has changed, `language` *explicitly* returns the obsolete version,
+// but `toLanguageTag()` uses the current version
+// https://developer.android.com/reference/java/util/Locale#getLanguage()
+val Locale.modernLanguageCode: String
+ get() {
+ return this.toLanguageTag().split('-', limit = 2)[0]
+ }
+
+fun Locale.getTuskyDisplayName(context: Context): String {
+ return context.getString(
+ R.string.language_display_name_format,
+ this?.displayLanguage,
+ this?.getDisplayLanguage(this)
+ )
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt b/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt
index 45f3ab371..6795317b2 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt
@@ -17,25 +17,89 @@ package com.keylesspalace.tusky.util
import android.content.Context
import android.content.SharedPreferences
-import android.content.res.Configuration
+import android.os.Build
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.os.LocaleListCompat
+import androidx.preference.PreferenceDataStore
import androidx.preference.PreferenceManager
-import java.util.Locale
+import com.keylesspalace.tusky.R
+import com.keylesspalace.tusky.settings.PrefKeys
+import javax.inject.Inject
+import javax.inject.Singleton
-class LocaleManager(context: Context) {
+@Singleton
+class LocaleManager @Inject constructor(
+ val context: Context
+) : PreferenceDataStore() {
private var prefs: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
- fun setLocale(context: Context): Context {
- val language = prefs.getNonNullString("language", "default")
- if (language == "default") {
- return context
- }
- val locale = Locale.forLanguageTag(language)
- Locale.setDefault(locale)
+ fun setLocale() {
+ val language = prefs.getNonNullString(PrefKeys.LANGUAGE, DEFAULT)
- val res = context.resources
- val config = Configuration(res.configuration)
- config.setLocale(locale)
- return context.createConfigurationContext(config)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ if (language != HANDLED_BY_SYSTEM) {
+ // app is being opened on Android 13+ for the first time
+ // hand over the old setting to the system and save a dummy value in Shared Preferences
+ applyLanguageToApp(language)
+
+ prefs.edit()
+ .putString(PrefKeys.LANGUAGE, HANDLED_BY_SYSTEM)
+ .apply()
+ }
+ } else {
+ // on Android < 13 we have to apply the language at every app start
+ applyLanguageToApp(language)
+ }
+ }
+
+ override fun putString(key: String?, value: String?) {
+
+ // if we are on Android < 13 we have to save the selected language so we can apply it at appstart
+ // on Android 13+ the system handles it for us
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ prefs.edit()
+ .putString(PrefKeys.LANGUAGE, value)
+ .apply()
+ }
+ applyLanguageToApp(value)
+ }
+
+ override fun getString(key: String?, defValue: String?): String? {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ val selectedLanguage = AppCompatDelegate.getApplicationLocales()
+
+ if (selectedLanguage.isEmpty) {
+ DEFAULT
+ } else {
+ // Android lets users select all variants of languages we support in the system settings,
+ // so we need to find the closest match
+ // it should not happen that we find no match, but returning null is fine (picker will show default)
+
+ val availableLanguages = context.resources.getStringArray(R.array.language_values)
+
+ return availableLanguages.find { it == selectedLanguage[0]!!.toLanguageTag() }
+ ?: availableLanguages.find { language ->
+ language.startsWith(selectedLanguage[0]!!.language)
+ }
+ }
+ } else {
+ prefs.getNonNullString(PrefKeys.LANGUAGE, DEFAULT)
+ }
+ }
+
+ private fun applyLanguageToApp(language: String?) {
+ val localeList = if (language == DEFAULT) {
+ LocaleListCompat.getEmptyLocaleList()
+ } else {
+ LocaleListCompat.forLanguageTags(language)
+ }
+
+ AppCompatDelegate.setApplicationLocales(localeList)
+ }
+
+ companion object {
+ private const val DEFAULT = "default"
+ private const val HANDLED_BY_SYSTEM = "handled_by_system"
}
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LocaleUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/LocaleUtils.kt
new file mode 100644
index 000000000..316e14d0a
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/util/LocaleUtils.kt
@@ -0,0 +1,89 @@
+/* Copyright 2022 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.util
+
+import android.util.Log
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.os.LocaleListCompat
+import com.keylesspalace.tusky.db.AccountEntity
+import java.util.Locale
+
+private const val TAG: String = "LocaleUtils"
+
+private fun mergeLocaleListCompat(list: MutableList, localeListCompat: LocaleListCompat) {
+ for (index in 0 until localeListCompat.size()) {
+ val locale = localeListCompat[index]
+ if (locale != null && list.none { locale.language == it.language }) {
+ list.add(locale)
+ }
+ }
+}
+
+// Ensure that the locale whose code matches the given language is first in the list
+private fun ensureLanguageIsFirst(locales: MutableList, language: String) {
+ var currentLocaleIndex = locales.indexOfFirst { it.language == language }
+ if (currentLocaleIndex < 0) {
+ // Recheck against modern language codes
+ // This should only happen when replying or when the per-account post language is set
+ // to a modern code
+ currentLocaleIndex = locales.indexOfFirst { it.modernLanguageCode == language }
+
+ if (currentLocaleIndex < 0) {
+ // This can happen when:
+ // - Your per-account posting language is set to one android doesn't know (e.g. toki pona)
+ // - Replying to a post in a language android doesn't know
+ locales.add(0, Locale(language))
+ Log.w(TAG, "Attempting to use unknown language tag '$language'")
+ return
+ }
+ }
+
+ if (currentLocaleIndex > 0) {
+ // Move preselected locale to the top
+ locales.add(0, locales.removeAt(currentLocaleIndex))
+ }
+}
+
+fun getInitialLanguage(language: String? = null, activeAccount: AccountEntity? = null): String {
+ return if (language.isNullOrEmpty()) {
+ // Account-specific language set on the server
+ if (activeAccount?.defaultPostLanguage?.isNotEmpty() == true) {
+ activeAccount.defaultPostLanguage
+ } else {
+ // Setting the application ui preference sets the default locale
+ AppCompatDelegate.getApplicationLocales()[0]?.language
+ ?: Locale.getDefault().language
+ }
+ } else {
+ language
+ }
+}
+
+fun getLocaleList(initialLanguage: String): List {
+ val locales = mutableListOf()
+ mergeLocaleListCompat(locales, AppCompatDelegate.getApplicationLocales()) // configured app languages first
+ mergeLocaleListCompat(locales, LocaleListCompat.getDefault()) // then configured system languages
+ locales.addAll( // finally, other languages
+ // Only "base" languages, "en" but not "en_DK"
+ Locale.getAvailableLocales().filter {
+ it.country.isNullOrEmpty() &&
+ it.script.isNullOrEmpty() &&
+ it.variant.isNullOrEmpty()
+ }.sortedBy { it.displayName }
+ )
+ ensureLanguageIsFirst(locales, initialLanguage)
+ return locales
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.java
index c94b42297..5b911fb13 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.java
+++ b/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.java
@@ -34,7 +34,10 @@ public class TimestampUtils {
public static String getRelativeTimeSpanString(Context context, long then, long now) {
long span = now - then;
boolean future = false;
- if (span < 0) {
+ if (Math.abs(span) < SECOND_IN_MILLIS) {
+ return context.getString(R.string.status_created_at_now);
+ }
+ else if (span < 0) {
future = true;
span = -span;
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt
index 3facc3a9a..bc40cdd6e 100644
--- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt
+++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt
@@ -47,6 +47,7 @@ fun Notification.toViewData(
this.type,
this.id,
this.account,
- this.status?.toViewData(isShowingContent, isExpanded, isCollapsed)
+ this.status?.toViewData(isShowingContent, isExpanded, isCollapsed),
+ this.report,
)
}
diff --git a/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewLayout.kt b/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewLayout.kt
new file mode 100644
index 000000000..802141f7d
--- /dev/null
+++ b/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewLayout.kt
@@ -0,0 +1,210 @@
+package com.keylesspalace.tusky.view
+
+import android.content.Context
+import android.graphics.Canvas
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+import com.keylesspalace.tusky.R
+import kotlin.math.roundToInt
+
+/**
+ * Lays out a set of [MediaPreviewImageView]s keeping their aspect ratios into account.
+ */
+class MediaPreviewLayout(context: Context, attrs: AttributeSet? = null) :
+ ViewGroup(context, attrs) {
+
+ private val spacing = context.resources.getDimensionPixelOffset(R.dimen.preview_image_spacing)
+
+ /**
+ * An ordered list of aspect ratios used for layout. An image view for each aspect ratio passed
+ * will be attached. Supports up to 4, additional ones will be ignored.
+ */
+ var aspectRatios: List = emptyList()
+ set(value) {
+ field = value
+ attachImageViews()
+ }
+
+ private val imageViewCache = Array(4) { MediaPreviewImageView(context) }
+
+ private var measuredOrientation = LinearLayout.VERTICAL
+
+ private fun attachImageViews() {
+ removeAllViews()
+ for (i in 0 until aspectRatios.size.coerceAtMost(imageViewCache.size)) {
+ addView(imageViewCache[i])
+ }
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val width = MeasureSpec.getSize(widthMeasureSpec)
+ val halfWidth = width / 2 - spacing / 2
+ var totalHeight = 0
+
+ when (childCount) {
+ 1 -> {
+ val aspect = aspectRatios[0]
+ totalHeight += getChildAt(0).measureToAspect(width, aspect)
+ }
+ 2 -> {
+ val aspect1 = aspectRatios[0]
+ val aspect2 = aspectRatios[1]
+
+ if ((aspect1 + aspect2) / 2 > 1.2) {
+ // stack vertically
+ measuredOrientation = LinearLayout.VERTICAL
+ totalHeight += getChildAt(0).measureToAspect(width, aspect1.coerceAtLeast(1.8))
+ totalHeight += spacing
+ totalHeight += getChildAt(1).measureToAspect(width, aspect2.coerceAtLeast(1.8))
+ } else {
+ // stack horizontally
+ measuredOrientation = LinearLayout.HORIZONTAL
+ val height = rowHeight(halfWidth, aspect1, aspect2)
+ totalHeight += height
+ getChildAt(0).measureExactly(halfWidth, height)
+ getChildAt(1).measureExactly(halfWidth, height)
+ }
+ }
+ 3 -> {
+ val aspect1 = aspectRatios[0]
+ val aspect2 = aspectRatios[1]
+ val aspect3 = aspectRatios[2]
+ if (aspect1 >= 1) {
+ // | 1 |
+ // -------------
+ // | 2 | 3 |
+ measuredOrientation = LinearLayout.VERTICAL
+ totalHeight += getChildAt(0).measureToAspect(width, aspect1.coerceAtLeast(1.8))
+ totalHeight += spacing
+ val bottomHeight = rowHeight(halfWidth, aspect2, aspect3)
+ totalHeight += bottomHeight
+ getChildAt(1).measureExactly(halfWidth, bottomHeight)
+ getChildAt(2).measureExactly(halfWidth, bottomHeight)
+ } else {
+ // | | 2 |
+ // | 1 |-----|
+ // | | 3 |
+ measuredOrientation = LinearLayout.HORIZONTAL
+ val colHeight = getChildAt(0).measureToAspect(halfWidth, aspect1)
+ totalHeight += colHeight
+ val halfHeight = colHeight / 2 - spacing / 2
+ getChildAt(1).measureExactly(halfWidth, halfHeight)
+ getChildAt(2).measureExactly(halfWidth, halfHeight)
+ }
+ }
+ 4 -> {
+ val aspect1 = aspectRatios[0]
+ val aspect2 = aspectRatios[1]
+ val aspect3 = aspectRatios[2]
+ val aspect4 = aspectRatios[3]
+ val topHeight = rowHeight(halfWidth, aspect1, aspect2)
+ totalHeight += topHeight
+ getChildAt(0).measureExactly(halfWidth, topHeight)
+ getChildAt(1).measureExactly(halfWidth, topHeight)
+ totalHeight += spacing
+ val bottomHeight = rowHeight(halfWidth, aspect3, aspect4)
+ totalHeight += bottomHeight
+ getChildAt(2).measureExactly(halfWidth, bottomHeight)
+ getChildAt(3).measureExactly(halfWidth, bottomHeight)
+ }
+ }
+
+ super.onMeasure(
+ widthMeasureSpec,
+ MeasureSpec.makeMeasureSpec(totalHeight, MeasureSpec.EXACTLY)
+ )
+ }
+
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+ val width = r - l
+ val height = b - t
+ val halfWidth = width / 2 - spacing / 2
+ when (childCount) {
+ 1 -> {
+ getChildAt(0).layout(0, 0, width, height)
+ }
+ 2 -> {
+ if (measuredOrientation == LinearLayout.VERTICAL) {
+ val y = imageViewCache[0].measuredHeight
+ getChildAt(0).layout(0, 0, width, y)
+ getChildAt(1).layout(
+ 0,
+ y + spacing,
+ width,
+ y + spacing + getChildAt(1).measuredHeight
+ )
+ } else {
+ getChildAt(0).layout(0, 0, halfWidth, height)
+ getChildAt(1).layout(halfWidth + spacing, 0, width, height)
+ }
+ }
+ 3 -> {
+ if (measuredOrientation == LinearLayout.VERTICAL) {
+ val y = getChildAt(0).measuredHeight
+ getChildAt(0).layout(0, 0, width, y)
+ getChildAt(1).layout(0, y + spacing, halfWidth, height)
+ getChildAt(2).layout(halfWidth + spacing, y + spacing, width, height)
+ } else {
+ val colHeight = getChildAt(0).measuredHeight
+ getChildAt(0).layout(0, 0, halfWidth, colHeight)
+ val halfHeight = colHeight / 2 - spacing / 2
+ getChildAt(1).layout(halfWidth + spacing, 0, width, halfHeight)
+ getChildAt(2).layout(
+ halfWidth + spacing,
+ halfHeight + spacing,
+ width,
+ colHeight
+ )
+ }
+ }
+ 4 -> {
+ val topHeight = (getChildAt(0).measuredHeight + getChildAt(1).measuredHeight) / 2
+ getChildAt(0).layout(0, 0, halfWidth, topHeight)
+ getChildAt(1).layout(halfWidth + spacing, 0, width, topHeight)
+ val bottomHeight =
+ (imageViewCache[2].measuredHeight + imageViewCache[3].measuredHeight) / 2
+ getChildAt(2).layout(
+ 0,
+ topHeight + spacing,
+ halfWidth,
+ topHeight + spacing + bottomHeight
+ )
+ getChildAt(3).layout(
+ halfWidth + spacing,
+ topHeight + spacing,
+ width,
+ topHeight + spacing + bottomHeight
+ )
+ }
+ }
+ }
+
+ inline fun forEachIndexed(action: (Int, MediaPreviewImageView) -> Unit) {
+ for (index in 0 until childCount) {
+ action(index, getChildAt(index) as MediaPreviewImageView)
+ }
+ }
+
+ override fun onDraw(canvas: Canvas?) {
+ super.onDraw(canvas)
+ }
+}
+
+private fun rowHeight(halfWidth: Int, aspect1: Double, aspect2: Double): Int {
+ return ((halfWidth / aspect1 + halfWidth / aspect2) / 2).roundToInt()
+}
+
+private fun View.measureToAspect(width: Int, aspect: Double): Int {
+ val height = (width / aspect).roundToInt()
+ measureExactly(width, height)
+ return height
+}
+
+private fun View.measureExactly(width: Int, height: Int) {
+ measure(
+ View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
+ View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
+ )
+}
diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java
index 2a25bb4cf..c70e2fc71 100644
--- a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java
+++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.java
@@ -17,8 +17,8 @@ package com.keylesspalace.tusky.viewdata;
import androidx.annotation.Nullable;
-import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Notification;
+import com.keylesspalace.tusky.entity.Report;
import com.keylesspalace.tusky.entity.TimelineAccount;
import java.util.Objects;
@@ -48,13 +48,16 @@ public abstract class NotificationViewData {
private final TimelineAccount account;
@Nullable
private final StatusViewData.Concrete statusViewData;
+ @Nullable
+ private final Report report;
public Concrete(Notification.Type type, String id, TimelineAccount account,
- @Nullable StatusViewData.Concrete statusViewData) {
+ @Nullable StatusViewData.Concrete statusViewData, @Nullable Report report) {
this.type = type;
this.id = id;
this.account = account;
this.statusViewData = statusViewData;
+ this.report = report;
}
public Notification.Type getType() {
@@ -74,6 +77,11 @@ public abstract class NotificationViewData {
return statusViewData;
}
+ @Nullable
+ public Report getReport() {
+ return report;
+ }
+
@Override
public long getViewDataId() {
return id.hashCode();
@@ -87,7 +95,8 @@ public abstract class NotificationViewData {
return type == concrete.type &&
Objects.equals(id, concrete.id) &&
account.getId().equals(concrete.account.getId()) &&
- (Objects.equals(statusViewData, concrete.statusViewData));
+ (Objects.equals(statusViewData, concrete.statusViewData)) &&
+ (Objects.equals(report, concrete.report));
}
@Override
@@ -97,7 +106,7 @@ public abstract class NotificationViewData {
}
public Concrete copyWithStatus(@Nullable StatusViewData.Concrete statusViewData) {
- return new Concrete(type, id, account, statusViewData);
+ return new Concrete(type, id, account, statusViewData, report);
}
}
diff --git a/app/src/main/res/drawable/ic_flag_24dp.xml b/app/src/main/res/drawable/ic_flag_24dp.xml
new file mode 100644
index 000000000..14529c0a9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_flag_24dp.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/play_indicator_overlay.xml b/app/src/main/res/drawable/play_indicator_overlay.xml
new file mode 100644
index 000000000..66ffc2c9b
--- /dev/null
+++ b/app/src/main/res/drawable/play_indicator_overlay.xml
@@ -0,0 +1,6 @@
+
+
+
+
\ 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 ec023062a..0c661b0b4 100644
--- a/app/src/main/res/layout-sw640dp/fragment_view_thread.xml
+++ b/app/src/main/res/layout-sw640dp/fragment_view_thread.xml
@@ -14,10 +14,10 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
- app:title="@string/title_view_thread"
- app:navigationIcon="?attr/homeAsUpIndicator"
+ app:menu="@menu/view_thread_toolbar"
app:navigationContentDescription="@string/abc_action_bar_up_description"
- app:menu="@menu/view_thread_toolbar" />
+ app:navigationIcon="?attr/homeAsUpIndicator"
+ app:title="@string/title_view_thread" />
@@ -25,8 +25,8 @@
android:id="@+id/swipeRefreshLayout"
android:layout_width="640dp"
android:layout_height="match_parent"
- app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
- android:layout_gravity="center_horizontal">
+ android:layout_gravity="center_horizontal|top"
+ app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
+ android:layout_gravity="center"
+ android:visibility="gone" />
diff --git a/app/src/main/res/layout/activity_followed_tags.xml b/app/src/main/res/layout/activity_followed_tags.xml
new file mode 100644
index 000000000..f26027571
--- /dev/null
+++ b/app/src/main/res/layout/activity_followed_tags.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 4e51b8b28..c0f583d64 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -28,20 +28,38 @@
-
+ android:orientation="horizontal">
+
+
+
+
+
@@ -61,13 +79,30 @@
app:contentInsetStart="0dp"
app:fabAlignmentMode="end">
-
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+
+
+
+
+
@@ -87,8 +122,8 @@
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:visibility="gone"
- android:layout_gravity="center" />
+ android:layout_gravity="center"
+ android:visibility="gone" />
diff --git a/app/src/main/res/layout/activity_tab_preference.xml b/app/src/main/res/layout/activity_tab_preference.xml
index 06b1d1c13..f1ebf9d8f 100644
--- a/app/src/main/res/layout/activity_tab_preference.xml
+++ b/app/src/main/res/layout/activity_tab_preference.xml
@@ -29,6 +29,7 @@
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
+ android:contentDescription="@string/action_add_tab"
android:src="@drawable/ic_plus_24dp" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/fragment_view_thread.xml b/app/src/main/res/layout/fragment_view_thread.xml
index b00fa7f81..faa4e4217 100644
--- a/app/src/main/res/layout/fragment_view_thread.xml
+++ b/app/src/main/res/layout/fragment_view_thread.xml
@@ -14,10 +14,10 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
- app:title="@string/title_view_thread"
- app:navigationIcon="?attr/homeAsUpIndicator"
+ app:menu="@menu/view_thread_toolbar"
app:navigationContentDescription="@string/abc_action_bar_up_description"
- app:menu="@menu/view_thread_toolbar" />
+ app:navigationIcon="?attr/homeAsUpIndicator"
+ app:title="@string/title_view_thread" />
@@ -25,8 +25,8 @@
android:id="@+id/swipeRefreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
- app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
- android:layout_gravity="top">
+ android:layout_gravity="top"
+ app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
+ android:layout_gravity="center"
+ android:visibility="gone" />
diff --git a/app/src/main/res/layout/item_add_or_remove_from_list.xml b/app/src/main/res/layout/item_add_or_remove_from_list.xml
new file mode 100644
index 000000000..732a883e7
--- /dev/null
+++ b/app/src/main/res/layout/item_add_or_remove_from_list.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/item_announcement.xml b/app/src/main/res/layout/item_announcement.xml
index fc7dbdb9e..6aa767517 100644
--- a/app/src/main/res/layout/item_announcement.xml
+++ b/app/src/main/res/layout/item_announcement.xml
@@ -20,8 +20,8 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="8dp"
- app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/text">
@@ -32,13 +31,13 @@
android:layout_centerVertical="true"
android:layout_marginTop="10dp"
android:contentDescription="@string/action_view_profile"
- tools:src="@drawable/avatar_default"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/notificationTextView" />
+ app:layout_constraintTop_toBottomOf="@id/notificationTextView"
+ tools:src="@drawable/avatar_default" />
+
+
-
-
-
+ app:srcCompat="@drawable/ic_check_24dp" />
diff --git a/app/src/main/res/layout/item_followed_hashtag.xml b/app/src/main/res/layout/item_followed_hashtag.xml
new file mode 100644
index 000000000..4866679b9
--- /dev/null
+++ b/app/src/main/res/layout/item_followed_hashtag.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
\ 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
index 3d89335fc..34a0376ce 100644
--- a/app/src/main/res/layout/item_media_preview.xml
+++ b/app/src/main/res/layout/item_media_preview.xml
@@ -1,198 +1,113 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/app/src/main/res/layout/item_report_notification.xml b/app/src/main/res/layout/item_report_notification.xml
new file mode 100644
index 000000000..19759f3c6
--- /dev/null
+++ b/app/src/main/res/layout/item_report_notification.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/account_toolbar.xml b/app/src/main/res/menu/account_toolbar.xml
index 8e0dc2421..bdcbc940a 100644
--- a/app/src/main/res/menu/account_toolbar.xml
+++ b/app/src/main/res/menu/account_toolbar.xml
@@ -18,6 +18,10 @@
android:title="@string/action_block"
app:showAsAction="never" />
+
+
@@ -29,4 +33,4 @@
-
\ No newline at end of file
+
diff --git a/app/src/main/res/menu/view_hashtag_toolbar.xml b/app/src/main/res/menu/view_hashtag_toolbar.xml
index 159593dc4..fb22c7e09 100644
--- a/app/src/main/res/menu/view_hashtag_toolbar.xml
+++ b/app/src/main/res/menu/view_hashtag_toolbar.xml
@@ -17,5 +17,18 @@
app:iconTint="?attr/colorOnSurface"
android:icon="@drawable/ic_person_remove_24dp" />
+
+
+
\ 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 e2c262a32..bdedf087b 100644
--- a/app/src/main/res/values-cs/strings.xml
+++ b/app/src/main/res/values-cs/strings.xml
@@ -2,29 +2,29 @@
Vyskytla se chyba.
Vyskytla se chyba sítě! Prosím zkontrolujte své připojení a zkuste to znovu!
- Tohle nemůže být prázdné.
- Neplatná doména zadána
- Autentizace s tímto serverem neuspěla.
- Nelze najít webový prohlížeč k použití.
+ Toto nemůže být prázdné.
+ Byla zadána neplatná doména
+ Autentizace s tímto serverem nebyla úspěšná.
+ Nepodařilo se najít webový prohlížeč, který lze použít.
Vyskytla se neidentifikovaná chyba autorizace.
Autorizace byla zamítnuta.
Nepodařilo se získat přihlašovací token.
- Toot je příliš dlouhý!
+ Příspěvek je příliš dlouhý!
Tento typ souboru nemůže být nahrán.
- Tento soubor nemohl být otevřen.
- Je vyžadováno povolení číst média.
- Je vyžadováno povolení ukládat média.
- K jednomu tootu nemohou být přiloženy obrázky i videa.
- Nahrání selhalo.
- Chyba při odesílání tootu.
+ Tento soubor se nepodařilo otevřít.
+ Je vyžadováno oprávnění ke čtení médií.
+ Je vyžadováno oprávnění ukládat média.
+ K jednomu příspěvku nemohou být přiloženy obrázky i videa.
+ Nahrání se nezdařilo.
+ Chyba při odesílání příspěvku.
Domů
Oznámení
Místní
- Federovaná
+ Federované
Přímé zprávy
Panely
- Toot
- Tooty
+ Vlákno
+ Příspěvky
S odpověďmi
Připnuté
Sledovaní
@@ -44,31 +44,31 @@
Zobrazit více
Zobrazit méně
Rozbalit
- Zabalit
+ Sbalit
Tady nic není.
- Tady nic není. Obnovte přetažnením dolů!
- %s boostnul/a váš toot
- %s si oblíbil/a váš toot
+ Tady nic není. Obnovte přetažením dolů!
+ %s boostnul/a váš příspěvek
+ %s si oblíbil/a váš příspěvek
%s vás nyní sleduje
Nahlásit uživatele @%s
- Dodatečné komentáře?
+ Další komentáře\?
Rychlá odpověď
Odpovědět
Boostnout
Odstranit boost
Oblíbit
Odstranit oblíbení
- Další
+ Více
Napsat
- Přihlásit účtem Mastodon
- Odhlásit
+ Přihlásit se účtem Mastodon
+ Odhlásit se
Jste si jistý/á, že se chcete odhlásit z účtu %1$s?
Sledovat
Přestat sledovat
Blokovat
Odblokovat
Skrýt boosty
- Zobrazi boosty
+ Zobrazit boosty
Nahlásit
Smazat
TOOTNOUT
@@ -85,10 +85,10 @@
Média
Otevřít v prohlížeči
Přidat média
- Požídit fotku
+ Pořídit fotku
Sdílet
Skrýt
- Odkrýt
+ Zrušit skrytí
Zmínit
Skrýt média
Otevřít menu
@@ -100,7 +100,7 @@
Zamítnout
Hledat
Koncepty
- Viditelnost tootu
+ Viditelnost příspěvku
Varování o obsahu
Klávesnice s emoji
Přidat panel
@@ -109,7 +109,7 @@
Hashtagy
Otevřít autora boostu
Zobrazit boosty
- Zobrazit oblíbené
+ Zobrazit oblíbení
Hashtagy
Zmínky
Odkazy
@@ -120,12 +120,12 @@
Sdílet jako…
Stáhnout média
Stahuji média
- Sdílet URL tootu na…
- Sdílet toot na…
+ Sdílet URL příspěvku na…
+ Sdílet příspěvek na…
Sdílet média na…
Odesláno!
- Uživatel odblokován
- Uživatel odkryt
+ Uživatel byl odblokován
+ Skrytí uživatele bylo zrušeno
Odesláno!
Odpověď byla úspěšně odeslána.
Který server?
@@ -138,7 +138,7 @@
Odpovědět…
Avatar
Záhlaví
- Co je server?
+ Co je to server\?
Připojuji se…
Sem může být zadána adresa či doména jakéhokoliv
serveru, například mastodon.social, icosahedron.website, social.tchncs.de
@@ -154,11 +154,11 @@
Stáhnout
Zrušit požadavek o sledování?
Přestat sledovat tento účet?
- Smazat tento toot?
+ Smazat tento příspěvek\?
Veřejný: Poslat na veřejné časové osy
Neuvedený: Neposlat na veřejné časové osy
Pouze pro sledující: Poslat pouze sledujícím
- Přímý: Poslat pouze zmíněným uživatelům
+ Přímé: Poslat pouze zmíněným uživatelům
Oznámení
Oznámení
Upozornění
@@ -177,7 +177,7 @@
Tmavý
Světlý
Černý
- Automatický při západu slunce
+ Automaticky při západu slunce
Použít systémový design
Prohlížeč
Používat Vlastní karty Chrome
@@ -185,7 +185,7 @@
Jazyk
Filtrování časových os
Panely
- Zobrazi boosty
+ Zobrazit boosty
Zobrazit odpovědi
Stahovat náhledy médií
Proxy
@@ -211,14 +211,16 @@
Noví sledující
Oznámení o nových sledujících
Boosty
- Oznámení, když jsou vaše tooty boostnuty
+ Oznámení, když jsou vaše příspěvky boostnuty
Oblíbení
- Oznámení, když jsou vaše tooty označeny jako oblíbené
+ Oznámení, když jsou vaše příspěvky označeny jako oblíbené
%s vás zmínil/a
%1$s, %2$s, %3$s a dalších %4$d
%1$s, %2$s a %3$s
%1$s a %2$s
+ - %d nová interakce
+ - %d nové interakce
- %d nových interakcí
Uzamčený účet
@@ -240,8 +242,8 @@
https://github.com/accelforce/Yuito/issues
Profil aplikace Yuito
- Sdílet obsah tootu
- Sdílet odkaz k tootu
+ Sdílet obsah příspěvku
+ Sdílet odkaz na příspěvek
Obrázky
Video
Vyžádáno sledování
@@ -249,7 +251,7 @@
za %d let
za %d d
za %d h
- za %d min
+ za %d m
za %d s
%d let
%d d
@@ -282,30 +284,35 @@
Hledejte mezi lidmi, které sledujete
Přidat účet na seznam
Odstranit účet ze seznamu
- Píšete s účtem %1$s
+ Píšete jako %1$s
Nastavení popisku selhalo
- - Popis pro zrakově postižené\n(limit %d znaků)
+ - Popis pro zrakově postižené
+\n(limit %d znak)
+ - Popis pro zrakově postižené
+\n(limit %d znaky)
+ - Popis pro zrakově postižené
+\n(limit %d znaků)
Nastavit popisek
Odstranit
Uzamknout účet
Vyžaduje, abyste ručně schvaloval/a sledující
Uložit koncept?
- Odesílám toot…
- Chyba při odesílání tootu
- Odesílám tooty
+ Odesílám příspěvek…
+ Chyba při odesílání příspěvku
+ Odesílám příspěvky
Odesílání bylo zrušeno
- Kopie vašeho tootu byla uložena do vašich konceptů
+ Kopie vašeho příspěvku byla uložena do vašich konceptů
Napsat
Vaše instance %s nemá žádná vlastní emoji
Styl emoji
Výchozí nastavení systému
Musíte si nejprve stáhnout tyto sady emoji
Provádím prohledávání…
- Rozbalit/zabalit všechny příspěvky
- Otevřít toot
- Je vyžadován restart aplikace
+ Rozbalit/Sbalit všechny příspěvky
+ Otevřít příspěvek
+ Je vyžadováno restartování aplikace
Pro použití těchto změn musíte restartovat aplikaci Yuito
Později
Restartovat
@@ -326,7 +333,7 @@
Označení
Obsah
Používat absolutní čas
- Níže uvedené informace nemusejí zcela odrážet profil uživatele. Dotknutím otevřete celý profil v prohlížeči.
+ Níže uvedené informace nemusejí zcela odrážet profil uživatele. Dotknutím se otevřete celý profil v prohlížeči.
Odepnout
Připnout
@@ -336,7 +343,7 @@
- %s boost
- - %s boost
+ - %s boosty
- %s boostů
Boostnuto uživatelem
@@ -345,35 +352,31 @@
%1$s a %2$s
%1$s, %2$s a %3$d další
- - bylo dosaženo maxima %1$d panelů
+ - bylo dosaženo maxima %1$d panelu
+ - bylo dosaženo maxima %1$d panelů
+
- Média %s
-
- Varování o obsahu: %s
-
- Žádný popis
-
- Boostnutý
-
- Oblíbený
-
+ Média %s
+ Varování o obsahu: %s
+ Žádný popis
+ Boostnutý
+ Oblíbený
Veřejný
Neuvedený
Pro sledující
- Přímý
-
+ Přímý
Název seznamu
Hashtag bez #
- Napsat toot
+ Napsat příspěvek
Napsat
- Vymazat
+ Vyčistit
Filtrovat
Použít
Zobrazovat indikátor pro roboty
Jste si jistý/á, že chcete trvale vymazat všechna vaše oznámení\?
Smazat a přepsat
- Smazat a přepsat tento toot\?
- %1$s • %2$s
+ Smazat a přepsat tento příspěvek\?
+ %1$s • %2$s
- %s hlas
- %s hlasy
@@ -403,30 +406,30 @@
- zbývá %d sekundy
- zbývá %d sekund
- Animovat avatary GIF
+ Animovat GIF avatary
Anketa s volbami: %1$s, %2$s, %3$s, %4$s; %5$s
Skryté domény
Skryté domény
Skrýt doménu %s
- Doména %s odkryta
+ Skrytí domény %s bylo zrušeno
Jste si jistý/á, že chcete zablokovat vše z domény %s\? Z této domény neuvidíte obsah v žádné veřejné časové ose ani v oznámeních. Vaši sledující z této domény budou odstraněni.
Skrýt celou doménu
Aktuální sada emoji od Googlu
Pokračovat
Zpět
Hotovo
- \@%s úspěšně nahlášen/a
- Dodatečné komentáře
+ \@%s byla/a úspěšně nahlášen/a
+ Další komentáře
Přeposlat na %s
Nahlášení selhalo
- Stahování tootů neuspělo
+ Stahování příspěvků selhalo
Nahlášení bude zasláno moderátorovi vašeho serveru. Níže můžete uvést, proč tento účet nahlašujete:
- Tento účet je z jiného serveru. Chcete na něj také poslat anonymizovanou kopii\?
+ Tento účet je z jiného serveru. Chcete na něj také poslat anonymizovanou kopii nahlášení\?
Zobrazit filtr oznámení
Anketa
5 minut
30 minut
- 1 hodinu
+ 1 hodina
6 hodin
1 den
3 dny
@@ -435,13 +438,13 @@
Lze zvolit více možností
Možnost %d
Upravit
- Plánované tooty
+ Naplánováné příspěvky
Upravit
Přidat anketu
- Plánované tooty
- Naplánovat toot
+ Naplánované příspěvky
+ Naplánované příspěvky
Obnovit
- Vždy rozbalovat tooty označené varováními o obsahu
+ Vždy rozbalovat příspěvky označené varováními o obsahu
Celé slovo
Je-li klíčové slovo nebo fráze pouze alfanumerická, bude použita pouze, pokud odpovídá celému slovu
Účty
@@ -460,7 +463,7 @@
Ukazovat náhledy k odkazům
Mastodon neumožňuje pracovat s intervalem menším než 5 minut.
Zatím zde nemáte žádné naplánované statusy.
- Zatím zde nejsou žádné koncepty.
+ Zatím zde nemáte žádné koncepty.
Možnost přetahování prstem pro přechod mezi kartami
Seznam
Přidat hashtag
@@ -471,21 +474,115 @@
Powered by Tusky
Zablokovat @%s\?
Nahoře
- Odkrýt %s
+ Zrušit skrytí domény %s
Skrýt oznámení od %s
- Odkrýt oznámení od %s
- Odkrýt %s
- Ztišit @%s\?
+ Zrušit skrytí oznámení od %s
+ Zrušit skrytí %s
+ Skrýt @%s\?
%s požádal/a aby vás mohl/a sledovat
Zobrazit dialogové okno s potvrzením při boostování
- %s právě vydal
+ %s právě zveřejnil/a příspěvek
Oznámení
Přihlášení
- %s se zaregistroval
- Přihlaste se znovu pro oznámení
- Nepodařilo se načíst stránku přihlášení.
- Tento příspěvek se nepodařilo poslat!
+ %s se zaregistroval/a
+ Přihlaste se znovu pro push oznámení
+ Nepodařilo se načíst přihlášovací stránku.
+ Tento příspěvek se nepodařilo odeslat!
Nepodařilo se načíst detaily účtu
Nepodařilo se načíst informace o odpovědi
Obrázek se nepodařilo upravit.
+
+ - %s osoba
+ - %s lidi
+ - %s lidí
+
+
+ - zbývá %d hodina
+ - zbývají %d hodiny
+ - zbývá %d hodin
+
+
+ - Nemůžete nahrát více než %1$d mediální přílohu.
+ - Nemůžete nahrát více než %1$d mediální přílohy.
+ - Nemůžete nahrát více než %1$d mediálních příloh.
+
+ Smazat tento naplánovaný příspěvek\?
+ Upozornění na nový toot někoho, koho sledujete.
+ Přihlášením souhlasíte s pravidly serveru %s.
+ Pravidla serveru %s
+ Některé informace, které mohou ovlivnit Vaši duševní pohodu, mohou být skryty. To zahrnuje:
+\n
+\n - Upozornění na boosty, oblíbené a sledování
+\n - Počty boostů a oblíbení u příspěvků
+\n - Statistiky sledujících a příspěvků na profilech
+\n
+\nPush oznámení nebudou ovlivněna, ale můžete si zkontrolovat jejich nastavení manuálně.
+ Znovu jste se přihlásili ke svému aktuálnímu účtu, abyste aplikaci Tusky udělili oprávnění k odběru push. Stále však máte další účty, které tímto způsobem migrovány nebyly. Přepněte se na ně a znovu se přihlaste na jednom po druhém, abyste povolili podporu oznámení UnifiedPush.
+ Uložit koncept\? (Přílohy budou znovu nahrány, když obnovíte koncept.)
+ Klepnutím nebo přetažením kruhu vyberte ohnisko, které bude vždy viditelné v miniaturách.
+ Před oblíbením zobrazit dialog pro potvrzení
+ Skrýt nadpis horního panelu nástrojů
+ Skrýt kvantitativní statistiky profilů
+ Opravdu chcete smazat seznam %s\?
+ I když váš účet není uzamčen, zaměstnanci %1$s si myslí, že byste mohli chtít zkontrolovat žádosti o sledování z těchto účtů ručně.
+ Odebírat
+ Přestat odebírat
+ Vytvořit příspěvek
+ Koncept byl smazán
+ Video a audio soubory nesmí překročit velikost %s MB.
+ Připnutí se nezdařilo
+ Zrušení připnutí se nezdařilo
+ Vždy
+ Nikdy
+ %s (%s)
+ Upravit obrázek
+ 14 dní
+ 30 dní
+ Příspěvek, na který jste připravili odpověď, byl odstraněn
+ %s (🔗 %s)
+ Nastavit bod zaostření
+ Nepodařilo se nastavit zaostřovací bod
+ Znovu se přihlaste ke všem účtům, abyste povolili podporu push oznámení.
+ Aby bylo možné používat push oznámení prostřednictvím UnifiedPush, Tusky potřebuje oprávnění k odběru oznámení na vašem serveru Mastodon. To vyžaduje opětovné přihlášení ke změně rozsahů OAuth udělených aplikaci Tusky. Použitím možnosti opětovného přihlášení zde nebo v předvolbách účtu zachováte všechny vaše místní koncepty a mezipaměť.
+ přidat reakci
+ příspěvek, se kterým jsem interagoval/a, je upraven
+ někdo se zaregistroval
+ někdo, ke komu jsem přihlášen/a, zveřejnil nový příspěvek
+ Když je přihlášeno více účtů
+ Nové příspěvky
+ Registrace
+ Oznámení o nových uživatelích
+ Úpravy příspěvků
+ Oznámení, když je upraven příspěvek, se kterým jste interagovala, je upraven
+ Audio
+ Přílohy
+ 1+
+ Jazyk příspěvku
+ Doba trvání
+ Na neurčito
+ 60 dní
+ 90 dní
+ 180 dní
+ 365 dní
+ (Beze změny)
+ Nejsou zde žádná oznámení.
+ Zobrazit uživatelské jméno v panelech nástrojů
+ Váše soukromá poznámka o tomto účtu.
+ Uloženo!
+ Pohoda
+ Zkontrolovat oznámení
+ Omezit upozornění na časové ose
+ Skrýt kvantitativní statistiky příspěvků
+ Připojil/a se %1$s
+ Koncept se ukládá…
+ Chyba při sledování #%s
+ Chyba při rušení sledování #%s
+ %s upravil/a svůj příspěvek
+ Odebrat záložku
+ Smazat konverzaci
+ Zavřít
+ Podrobnosti
+ Smazat tuto konverzaci\?
+ Požádáno o sledování
+ Animovat vlastní emotikony
diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml
index 1c38634bd..d4b88cb4d 100644
--- a/app/src/main/res/values-cy/strings.xml
+++ b/app/src/main/res/values-cy/strings.xml
@@ -20,7 +20,7 @@
Hysbysiadau
Lleol
Ffedereiddwyd
- Edau
+ Neges
Negeseuon
Gydag ymatebion
Dilyniadau
@@ -32,7 +32,7 @@
Golygu\'ch proffil
Drafftiau
Trwyddedau
- %s wedi\'u hybu
+ Wedi\'i hybu gan %s
Cynnwys sensitif
Cyfryngau wedi\'u cudd
Cliciwch i weld
@@ -41,9 +41,9 @@
Chwyddo
Lleihau
Dim byd yma. Tynnwch lawr i adnewyddu!
- %s wedi hybu\'ch post
- %s wedi hoffi\'ch post
- %s wedi\'ch dilyn chi
+ Mae %s wedi hybu\'ch post
+ Mae %s wedi hoffi\'ch post
+ Mae %s wedi\'ch dilyn chi
Adrodd @%s
Sylwadau ychwanegol?
Ateb Cyflym
@@ -309,7 +309,7 @@
Tewi sgwrs
Hidlo
Hysbysiadau am bolau sydd wedi cwblhau
- Ymunwyd %1$s
+ Ymunodd %1$s
Does gennych ddim negeseuon arfaethedig.
%s (%s)
Ydych chi\'n siŵr eich bod chi am glirio\'ch holl hysbysiadau yn barhaol\?
diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml
index 973abcf9f..957b4c65d 100644
--- a/app/src/main/res/values-de/strings.xml
+++ b/app/src/main/res/values-de/strings.xml
@@ -10,12 +10,12 @@
Autorisierung wurde abgelehnt.
Es konnte kein Login-Token abgerufen werden.
Der Beitrag ist zu lang!
- Dieser Dateityp darf nicht hochgeladen werden.
+ Dieser Dateityp kann nicht hochgeladen werden.
Die Datei konnte nicht geöffnet werden.
- Eine Leseberechtigung wird für das Hochladen der Mediendatei benötigt.
+ Berechtigung für Zugriff auf Mediendateien benötigt.
Eine Berechtigung wird zum Speichern des Mediums benötigt.
Bilder und Videos können nicht an den gleichen Beitrag angehängt werden.
- Die Mediendatei konnte nicht hochgeladen werden.
+ Das Hochladen ist gescheitert.
Fehler beim Senden des Beitrags.
Start
Benachrichtigungen
@@ -25,7 +25,7 @@
Tabs
Konversation
Beiträge
- mit Antworten
+ Mit Antworten
Angeheftet
Folgt
Folgende
@@ -43,32 +43,32 @@
Zum Anzeigen tippen
Zeige mehr
Zeige weniger
- Mehr
- Weniger
+ Ausklappen
+ Einklappen
Hier ist nichts.
Noch keine Beiträge hier! Ziehe nach unten um zu aktualisieren!
%s teilte deinen Beitrag
%s favorisierte deinen Beitrag
%s folgt dir
\@%s melden
- Irgendwelche Anmerkungen?
+ Zusätzliche Anmerkungen\?
Schnell antworten
Antworten
- Boosten
- Boost entfernen
+ Teilen
+ Teilen rückgängig machen
Favorisieren
Favorisierung entfernen
Mehr
Beitrag erstellen
Mit Mastodon anmelden
Ausloggen
- Bist du sicher, dass du dich aus dem Konto %1$s ausloggen möchtest\?
+ Bist du sicher, dass du dich vom Konto %1$s abmelden möchtest\?
Folgen
Entfolgen
Blockieren
Entblockieren
Geteilte Beiträge verbergen
- Zeige Boosts
+ Zeige geteilte Beiträge
Melden
Löschen
TRÖT
@@ -116,12 +116,12 @@
%1$s heruntergeladen
Link kopieren
Öffne als %s
- Teilen als …
- Beitragslink teilen…
- Beitragsinhalt teilen…
- Mediendatei teilen…
+ Teilen als …
+ Beitragslink teilen …
+ Beitragsinhalt teilen …
+ Mediendatei teilen …
Gesendet!
- entblockt
+ Benutzer entblockt
Stummschaltung aufgehoben
Gesendet!
Antwort erfolgreich gesendet.
@@ -130,13 +130,13 @@
Inhaltswarnung
Anzeigename
Über mich
- Mastodon durchsuchen…
+ Suchen …
Keine Ergebnisse
- Antworten…
+ Antworten …
Profilbild
Titelbild
- Was ist eine Instanz?
- Verbinden …
+ Was ist eine Instanz\?
+ Verbinde …
Die Adresse einer Instanz oder Domain kann
hier eingegeben werden, wie z.B. mastodon.social, icosahedron.website, social.tchncs.de, und
mehr!
@@ -147,7 +147,7 @@
\n\nWeitere Informationen gibt es auf joinmastodon.org.
Stelle Medienupload fertig
- Lade hoch …
+ Lade hoch …
Herunterladen
Folgeanfrage zurückziehen?
Willst du diesem Profil wirklich nicht mehr folgen?
@@ -165,8 +165,8 @@
Benachrichtigen wenn
Ich erwähnt werde
Mir jemand folgt
- Jemand meine Posts teilt
- Jemandem meine Posts gefallen
+ Jemand meine Beiträge teilt
+ Jemandem meine Beiträge gefallen
Aussehen
App-Thema
Zeitleisten
@@ -262,14 +262,17 @@
Veröffentlichen als %1$s
Fehler beim Speichern der Beschreibung
- - Für Menschen mit Sehbehinderung beschreiben\n(%d Zeichen)
+ - Für Mensch mit Sehbehinderung beschreiben
+\n(%d Zeichen)
+ - Für Menschen mit Sehbehinderung beschreiben
+\n(%d Zeichen)
Beschreibung eingeben
Entfernen
Gesperrtes Profil
Wer dir folgen möchte, muss um deine Erlaubnis bitten
Entwurf speichern?
- Beitrag senden…
+ Sende Beitrag …
Fehler beim Senden
Beiträge senden
Senden abgebrochen
@@ -279,7 +282,7 @@
Emoji-Stil
System-Standard
Du musst diese Emoji-Sets zunächst herunterladen
- Nachschlagen…
+ Schlage nach …
Alle Beiträge aus-/einklappen
Beitrag öffnen
App-Neustart erforderlich
@@ -419,7 +422,7 @@
Mehrere Möglichkeiten
Möglichkeit %d
Geplante Beiträge
- Editieren
+ Bearbeiten
Geplante Beiträge
Plane Beitrag
Zurücksetzen
@@ -430,7 +433,7 @@
Als Lesezeichen gespeichert
Liste auswählen
Liste
- Fehler beim Nachschlagen von Post %s
+ Fehler beim Nachschlagen von Beitrag %s
Du hast keine Entwürfe.
Du hast keine geplanten Beiträge.
Das Datum des geplanten Toots muss mindestens 5 Minuten in der Zukunft liegen.
@@ -490,7 +493,7 @@
Neue Beiträge
GIF-Emojis animieren
Jemand, den ich abonniert habe, hat etwas Neues veröffentlicht
- %s hat gerade etwas gepostet
+ %s hat gerade etwas veröffentlicht
%d Min.
Benachrichtigungen überprüfen
Informationen, die dein Wohlbefinden beeinflussen könnten, werden versteckt. Das beinhaltet
@@ -502,7 +505,7 @@
\nPush-Benachrichtigungen sind nicht betroffen, aber du kannst diese manuell überprüfen.
Auch wenn dein Konto nicht gesperrt ist, haben die Admins von %1$s gedacht, dass es besser wäre diese Folgenden manuell zu bestätigen.
Keine Statistiken auf Profilen zeigen
- Keine Statistiken in Posts zeigen
+ Keine Statistiken in Beiträgen zeigen
Timeline-Benachrichtigungen einschränken
Abonnieren
nicht mehr abonnieren
@@ -540,12 +543,31 @@
Melde alle Konten neu an, um die Unterstützung für Push-Benachrichtigungen zu aktivieren.
%1$s beigetreten
1+
+ Jetzt
Fehler beim Laden der Kontodetails
Bild bearbeiten
Details
Das Bild konnte nicht bearbeitet werden.
- Speichere den Entwurf…
+ Speichere Entwurf …
Video- oder Tondateien dürfen nicht grösser als %s MB sein.
#%s folgen fehlgeschlagen
#%s entfolgen fehlgeschlagen
+ Diesen geplanten Beitrag löschen\?
+ %s-Regeln
+ Mit dem Anmelden stimmst du den Regeln von %s zu.
+ Tippe oder ziehe den Kreis auf die Stelle, die in Vorschaubildern in der Mitte sein soll.
+ Entwurf speichern\? (Anhänge werden erneut hochgeladen, sobald du den Entwurf wiederherstellst.)
+ (Keine Änderung)
+ Benutzername in Hauptnavigation anzeigen
+ Pinnen fehlgeschlagen
+ Lösen fehlgeschlagen
+ Immer
+ Wenn mit mehreren Konten angemeldet
+ Niemals
+ %s (%s)
+ Sprache des Beitrags
+ %s (🔗 %s)
+ Setzen des Fokuspunktes fehlgeschlagen
+ Fokuspunkt setzen
+ Reaktion hinzufügen
diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml
index 110c5c245..48ceb2156 100644
--- a/app/src/main/res/values-eo/strings.xml
+++ b/app/src/main/res/values-eo/strings.xml
@@ -148,11 +148,11 @@
\n
\nPliaj informoj troviĝas ĉe joinmastodon.org.
Finante alŝuto de aŭdovidaĵojn
- Alŝutante…
+ Alŝutado…
Elŝuti
- Nuligi peton de sekvado?
- Ne plu sekvi?
- Forigi ĉi tiun mesaĝon?
+ Ĉu nuligi peton de sekvado\?
+ Ĉu ne plu sekvi\?
+ Ĉu forigi ĉi tiun mesaĝon\?
Publika: afiŝi en publikaj tempolinioj
Nelistigita: Ne afiŝi en publikaj tempolinioj
Nur por sekvantoj: Afiŝi nur al sekvantoj
@@ -163,7 +163,7 @@
Sciigi per sono
Sciigi per vibro
Sciigi per lumo
- Sciigi al mi kiam
+ Sciigi al mi, kiam
iu mencias min
iu sekvas min
miaj mesaĝoj estas diskonigitaj
@@ -176,10 +176,10 @@
Hela
Nigra
Aŭtomata laŭ la horo
- Uzi sisteman temon
+ Uzi sisteman etoson
Retumilo
Uzi la integritan retumilon
- Kâsi butonon de verko dum rulumado
+ Kaŝi butonon de verko dum rulumado
Lingvo
Filtrado de tempolinioj
Langetoj
@@ -187,14 +187,14 @@
Montri la respondojn
Elŝuti antaŭvidojn de aŭdovidaĵoj
Prokurilo
- HTTP prokurilo
- Ebligi HTTP prokurilon
- Adreso de HTTP prokurilo
- Pordo de HTTP prokurilo
+ HTTP-prokurilo
+ Ebligi HTTP-prokurilon
+ Adreso de HTTP-prokurilo
+ Pordo de HTTP-prokurilo
Dekomenca privateco de mesaĝoj
- Ĉiam marki aŭdovidaĵojn kiel tiklaj
+ Ĉiam marki aŭdovidaĵojn kiel tiklajn
Publikigante (sinkronigita kun la servilo)
- Sinkronigo de la preferoj malsukcesis
+ Sinkronigo de la agordoj malsukcesis
Publika
Nelistigita
Nur por sekvantoj
@@ -205,16 +205,16 @@
Granda
La plej granda
Novaj mencioj
- Sciigoj pri novajn menciojn
+ Sciigoj pri novaj mencioj
Novaj sekvantoj
- Sciigoj pri novajn sekvantojn
+ Sciigoj pri novaj sekvantoj
Diskonigoj
- Sciigoj kiam viaj mesaĝoj estas diskonigita
+ Sciigoj, kiam viaj mesaĝoj estas diskonigitaj
Stelumoj
- Sciigoj kiam viaj mesaĝoj estas stelumitaj
+ Sciigoj, kiam viaj mesaĝoj estas stelumitaj
%s menciis vin
%1$s, %2$s, %3$s kaj %4$d aliaj
- %1$s, %2$s, kaj %3$s
+ %1$s, %2$s kaj %3$s
%1$s kaj %2$s
- %d nova interago
@@ -232,12 +232,10 @@
to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html
* the url can be changed to link to the localized version of the license.
-->
- Paĝaro de projekto:\n
- https://accelf.net/yuito
-
- Raportoj de cimo kaj petoj de funkcio:\n
- https://github.com/accelforce/Yuito/issues
-
+ Paĝaro de la projekto:
+\n https://accelf.net/yuito
+ Raportoj de cimoj kaj petoj de funkcioj:
+\n https://github.com/accelforce/Yuito/issues
Profilo de Yuito
Konigi enhavon de la mesaĝo
Konigi ligilon al mesaĝo
@@ -245,11 +243,11 @@
Video
Sekvado petita
- en %dj
- en %dt
- en %dh
- en %dm
- en %ds
+ post %dj
+ post %dt
+ post %dh
+ post %dm
+ post %ds
%dj
%dt
%dh
@@ -260,15 +258,15 @@
Aŭdovidaĵoj
Respondo al @%s
ŝarĝi pli
- Publikaj liniotempoj
+ Publikaj tempolinioj
Konversacioj
Aldoni filtrilon
Redakti filtrilon
Forigi
- Akualigi
+ Aktualigi
Frazo filtrota
Aldoni konton
- Aldoni novan Mastodon konton
+ Aldoni novan Mastodon-konton
Listoj
Listoj
Ne povis krei la liston
@@ -278,30 +276,32 @@
Ŝanĝi la nomon de la listo
Forigi la liston
Redakti la liston
- Serĉi homojn ke vi sekvas
+ Serĉi homojn, kiujn vi sekvas
Aldoni konton al la listo
Forigi konton el la listo
Afiŝi per konto %1$s
Redakto de apudskribo malsukcesis
- - Priskribi por misvidantaj homoj\n(%d signoj maksimume)
+
+ - Priskribi por vide handikapitaj homoj
+\n(%d signoj maksimume)
Redakti apudskribon
Forigi
Ŝlosi konton
Vi devas permane rajtigi sekvantojn
- Konservi malneton?
- Sendante la mesaĝo…
+ Ĉu konservi malneton\?
+ Sendado de la mesaĝo…
Eraro dum sendo de la mesaĝo
- Sendante la mesaĝoj
+ Sendado de la mesaĝoj
Sendo nuligita
Kopio de la mesaĝo estis konservita en viaj malnetoj
Verki
Via nodo %s ne havas proprajn emoĝiojn
Stilo de emoĝioj
- Sistema valoro
- Vi unue devos elŝuti ĉi tiujn emoĝiarojn
- Serĉante…
+ El la sistemo
+ Vi unue devos elŝuti ĉi tiun emoĝiaron
+ Serĉado…
Pligrandigi/malgrandigi ĉiujn mesaĝojn
Malfermi mesaĝon
Restartigo necesas
@@ -309,15 +309,15 @@
Poste
Restartigi
Dekomenca emoĝiaro de via aparato
- La emoĝioj «Blob» konataj el Android 4.4−7.1
+ La emoĝioj «Blob» konataj de Android 4.4−7.1
Norma emoĝiaro de Mastodon
Elŝuto malsukcesis
Roboto
%1$s moviĝis al:
Diskonigi al la originala atentaro
- Eksdiskonigi
+ Maldiskonigi
Yuito enhavas kodon kaj risurcojn el la sekvantaj malfermitkodaj projetkoj:
- Laŭ la permesilo «Apache License» (kopio sube)
+ Laŭ la permesilo «Apache» (kopio sube)
CC-BY 4.0
CC-BY-SA 4.0
Profilaj metadatumoj
@@ -325,7 +325,7 @@
Etikedo
Enhavo
Uzi absolutan tempon
- Subaj informoj povas nekomplete prezenti la profilon de la uzanto. Presi por malfermi la kompletan profilon en retumilo.
+ Subaj informoj povas nekomplete prezenti la profilon de la uzanto. Tuŝi por malfermi la kompletan profilon en retumilo.
Depingli
Alpingli
@@ -337,7 +337,7 @@
- <b>%s</b> Diskonigoj
Diskonigita de
- Stelumita per
+ Stelumita de
%1$s
%1$s kaj %2$s
%1$s, %2$s kaj %3$d aliaj
@@ -363,19 +363,19 @@
Rekta
Nomo de la listo
Forigi kaj reskribi
- Ĉu forigi kaj reskribi ĉi-tiun mesaĝon\?
+ Ĉu forigi kaj reskribi ĉi tiun mesaĝon\?
enketoj finiĝis
- Montri indikilon por robotoj
- Moviĝi GIF profilbildojn
+ Montri indikilon pri robotoj
+ Ebligi GIF-profilbildojn
Enketoj
- Sciigoj pri enketoj kiuj finiĝis
+ Sciigoj pri enketoj, kiuj finiĝis
Kradvorto sen #
Viŝi
Filtri
Apliki
Verki mesaĝon
Verki
- Ĉu vi certas ke vi volas proĉiame viŝi ĉiujn viajn sciigojn\?
+ Ĉu vi certas, ke vi volas porĉiame viŝi ĉiujn viajn sciigojn\?
Agoj por bildo %s
%1$s • %2$s
@@ -383,15 +383,15 @@
- %s voĉdonoj
finiĝos je %s
- finiĝita
+ finita
Voĉdoni
- Enketo al kiu vi voĉdonis finiĝis
- Enketo kiu vi kreis finiĝis
+ Enketo, al kiu vi voĉdonis, finiĝis
+ Enketo, kiun vi kreis, finiĝis
Kaŝitaj domajnoj
Kaŝitaj domajnoj
Silentigi %s
%s malsilentigita
- Ĉu vi certas ke vi volas tute bloki %s\? Vi ne vidos enhavon de tiu domajno en publika tempolinio aŭ en viaj sciigoj. Viaj sekvantoj de tiu domajno estos forigitaj.
+ Ĉu vi certas ke vi volas tute bloki %s\? Vi ne vidos enhavon de tiu domajno en publika tempolinio aŭ en viaj sciigoj. Viaj sekvantoj el tiu domajno estos forigitaj.
Kaŝi la tutan domajnon
La aktuala emoĝiaro de Google
Balotenketo kun elektoj: %1$s, %2$s, %3$s, %4$s, %5$s
@@ -405,14 +405,14 @@
Venigo de statusoj malsukcesis
La signalo estos sendita al la kontrolantoj de via servilo. Vi povas doni klarigon pri kial vi signalas ĉi tiun konton sube:
La konto estas en alia servilo. Ĉu sendi sennomigitan kopion de la signalo ankaŭ tien\?
- Montri filtrilon de Sciigoj
+ Montri filtrilon de sciigoj
Tuta vorto
- Kiam ĉefvorto aŭ frazo estas nur litercifera, tio aplikos nur se ĝi kongruas la tutan vorton
+ Ŝlosilvorto aŭ frazo litercifera aplikiĝos, nur se ĝi kongruas kun la tuta vorto
Kontoj
Serĉo malsukcesis
Aldoni baloton
- Ĉiam pligrandigi tootoj markiĝita per enhavaj avertoj
- Baloto
+ Ĉiam montri mesaĝojn kun enhavaj avertoj
+ Enketo
5 minutoj
30 minutoj
1 horo
@@ -436,9 +436,9 @@
Aldonita al la legosignoj
Elekti la liston
Listo
- Eraro dum elserĉo de la mesaĝo %s
- Vi ne havas iun ajn malneton.
- Vi ne havas iun ajn planitan mesaĝon.
+ Eraro dum serĉo de la mesaĝo %s
+ Vi havas neniun malneton.
+ Vi havas neniun planitan mesaĝon.
Petoj de sekvado
Kradvortoj
@@ -449,8 +449,8 @@
Sciigoj pri petoj de sekvado
Montri buntajn transirojn por kaŝitaj aŭdovidaĵoj
Kaŝi la sciigojn
- Silentigi @%s\?
- Bloki @%s\?
+ Ĉu silentigi @%s\?
+ Ĉu bloki @%s\?
Malsilentigi la konversacion
Silentigi la konversacion
Malsilentigi %s
@@ -488,48 +488,48 @@
Ebligi ŝovumadon por ŝanĝi inter la langetoj
Mastodon havas minimuman intervalon de planado de 5 minutoj.
Kunsendaĵoj
- iu kiun mi sekvas afiŝis novan mesaĝon
+ iu, kiun mi sekvas, afiŝis novan mesaĝon
Ĉu vi vere volas forigi la liston %s\?
- Aŭdio
+ Aŭdaĵo
Aboni
Malneto forigita
- - Vi ne povas elŝuti pli ol %1$d aŭdovidaĵa kunsendaĵo.
- - Vi ne povas elŝuti pli ol %1$d aŭdovidaĵaj kunsendaĵoj.
+ - Vi ne povas elŝuti pli ol %1$d aŭdovida kunsendaĵo.
+ - Vi ne povas elŝuti pli ol %1$d aŭdovidaj kunsendaĵoj.
Daŭro
Nedefinita
Malaboni
Novaj mesaĝoj
Forigi la legosignon
- Ĉu forigi ĉi-tiun konversacion\?
+ Ĉu forigi ĉi tiun konversacion\?
Animacii proprajn emoĝiojn
- Kaŝi kvantecajn statistikaĵojn sur la profiloj
+ Kaŝi kvantecajn statistikaĵojn pri la profiloj
Forigi konversacion
%s ĵus afiŝis
- Sciigoj kiam iu kiun vi sekvas afiŝis novan mesaĝon
- Sendo de ĉi-tiu mesaĝo malsukcesis!
- Kaŝi kvantecajn statistikaĵojn sur la mesaĝoj
+ Sciigoj, kiam iu, kiun vi sekvas, afiŝis novan mesaĝon
+ Sendo de ĉi tiu mesaĝo malsukcesis!
+ Kaŝi kvantecajn statistikaĵojn pri la mesaĝoj
Demandi konfirmon antaŭ ol stelumi
Bonstato
Ŝarĝado de respondaj informoj malsukcesis
Kelkaj informoj kiuj povas afekci vian mensan bonstaton estos kaŝitaj. Ĉi tiuj inkluzivas:
\n
-\n - Sciigoj pri stelumo/diskonigo/sekvado
-\n- Nombro de stelumoj/diskonigoj sur la mesaĝoj
-\n- Statistikoj pri mesaĝoj/sekvantoj sur la profiloj
+\n — Sciigoj pri stelumo/diskonigo/sekvado
+\n — Nombro de stelumoj/diskonigoj sur la mesaĝoj
+\n — Statistikoj pri mesaĝoj/sekvantoj sur la profiloj
\n
-\n Puŝosciigoj ne estos influitaj, sed vi povas kontroli viajn sciigojn preferojn permane.
+\n Sciigoj ne estos influitaj, sed vi povas kontroli viajn agordojn pri sciigojn permane.
Kontroli la sciigojn
Limigi sciigojn pri tempolinio
- La mesaĝo al kiu ĉi tiu malneto respondas estis forigita
+ La mesaĝo, al kiu tiu ĉi malneto respondas, estis forigita
%s registriĝis
iu registriĝis
- mesaĝo kun kiu mi interagis estas redaktita
+ mesaĝo, kun kiu mi interagis, estas redaktita
Novaj kontoj
Sciigoj pri novaj uzantoj
1+
- Kvankam via konto ne estas blokita, la teamo de %1$s pensas ke vi eble volus permane validigi la sekvopetojn de tiuj ĉi kontoj.
+ Kvankam via konto ne estas ŝlosita, la teamo de %1$s pensas, ke vi eble volus permane validigi la sekvopetojn de tiuj ĉi kontoj.
Filmetoj kaj sondosieroj ne povas esti pli grandaj ol %s MB.
La bildo ne povis esti redaktita.
Ensaluti
@@ -540,7 +540,7 @@
Fermi
Detaloj
Redaktitaj mesaĝoj
- Sciigoj kiam mesaĝoj kun kiuj vi interagis estas redaktitaj
+ Sciigoj, kiam mesaĝoj, kun kiuj vi interagis, estas redaktitaj
Redakti la bildon
14 tagoj
30 tagoj
@@ -550,8 +550,21 @@
365 tagoj
Ekverki mesaĝon
Aliĝis je %1$s
- Registras la malneton…
+ Konservado de la malneto…
Ensalutu denove al ĉiuj kontoj por ŝalti sciigojn.
La salutpaĝo ne povis esti ŝargita.
Ŝargo de detaloj pri la konto malsukcesis
+ Ĉu forigi tiun planitan mesaĝon\?
+ Si vi ensalutas, vi konsentas je la regulo de %s.
+ Regulo de %s
+ (Neniu ŝanĝo)
+ %s (%s)
+ Ĉiam
+ Kiam vi uzas plurajn kontojn
+ Neniam
+ Montri uzantnomon en ilobreto
+ Mesaĝolingvo
+ Por ricevi sciigoj per UnifiedPush, Tusky bezonas taŭgan permeson el Mastodon-servilo. Tio postulas re-ensaluton por ŝanĝi OAuth-rajtoj donitaj al Tusky. Se vi uzas la opcion re-ensaluti ĉi tie aŭ en la agordoj de la konto, viaj malnetoj kaj kaŝmemoroj estos konservitaj.
+ Vi re-ensalutis en tiu konto por doni sciigo-permeson al Tusky. Vi havas tamen aliajn kontojn, ĉe kiuj vi devas re-sensaluti. Iru al ili, kaj re-ensalutu por ebligi ricevon de sciigoj per UnifiedPush.
+ %s (🔗 %s)
diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml
index 3c292b45d..3361799d3 100644
--- a/app/src/main/res/values-es/strings.xml
+++ b/app/src/main/res/values-es/strings.xml
@@ -9,12 +9,12 @@
Ocurrió un error de autorización no identificado.
La autorización falló.
Fallo al obtener identificador de login.
- ¡El estado es demasiado largo!
+ ¡La publicación es demasiado larga!
No se admite este tipo de archivo.
No pudo abrirse el fichero.
Se requiere permiso para acceder al almacenamiento.
Se requiere permiso para descargar al almacenamiento.
- No se pueden adjuntar imágenes y vídeos en el mismo estado.
+ No se pueden adjuntar imágenes y vídeos en la misma publicación.
La subida falló.
Error al publicar.
Inicio
@@ -23,7 +23,7 @@
Federada
Mensajes Directos
Pestañas
- Publicación
+ Hilo
Estados
Con respuestas
Fijado
@@ -47,8 +47,8 @@
Ocultar
Nada aquí.
Nada por aquí. ¡Arrastra hacia abajo para recargar!
- %s impulsó tu toot
- %s marcó favorito
+ %s impulsó tu publicación
+ %s marcó como favorita tu publicación
%s te siguió
Reportar @%s
¿Información adicional?
@@ -98,7 +98,7 @@
Rechazar
Buscar
Borradores
- Visibilidad del estado
+ Visibilidad de la publicación
Aviso de contenido
Teclado de emojis
Añadir pestaña
@@ -194,15 +194,16 @@
Nuevos seguidores
Notificaciones de nuevos seguidores
Impulsos
- Notificaciones de estados que fueron compartidos
+ Notificaciones cuando impulsan tus publicaciones
Favoritos
- Notificaciones de estados que recibieron favorito
+ Notificaciones de tus estados marcados como favorito
%s te mencionó
%1$s, %2$s, %3$s y %4$d otros
%1$s, %2$s, y %3$s
%1$s y %2$s
- %d nueva interacción
+ - %d nuevas interacciones
- %d nuevas interacciones
Cuenta protegida
@@ -230,6 +231,7 @@
Video
Solicitud enviada
+ Ahora
en %dy
en %dd
en %dh
@@ -249,10 +251,15 @@
Añadir cuenta de Mastodon
Listas
Listas
- Publicando con la cuenta %1$s
+ Publicar como %1$s
Error al añadir leyenda
- - Describir para invidentes\n(límite de %d caracteres)
+ - Descripción para personas con problemas de visión
+\n(Límite de %d caracter)
+ - Descripción para personas con problemas de visión
+\n(Límite de %d caracteres)
+ - Descripción para personas con problemas de visión
+\n(Límite de %d caracteres)
Añadir leyenda
Eliminar
@@ -297,12 +304,14 @@
No fijar
Fijar
- - <b>%1$s</b> Favorito
- - <b>%1$s</b> Favoritos
+ - %1$s Favorito
+ - %1$s Favoritos
+ - %1$s Favoritos
- - %s impulso
- - %s impulsos
+ - %s Impulso
+ - %s Impulsos
+ - %s Impulsos
Impulsado por
Marcado como favorito por
@@ -311,6 +320,7 @@
%1$s, %2$s y %3$d más
- máximo de %1$d pestaña alcanzada
+ - máximo de %1$d pestañas alcanzadas
- máximo de %1$d pestañas alcanzadas
Menciones
@@ -335,23 +345,28 @@
Votar
- %d día restante
+ - %d días restantes
- %d días restante
- %d hora restante
- - %d horas restante
+ - %d horas restantes
+ - %d horas restantes
- %d minuto restante
- - %d minutos restante
+ - %d minutos restantes
+ - %d minutos restantes
- %d segundo restante
- - %d segundos restante
+ - %d segundos restantes
+ - %d segundos restantes
%1$s • %2$s
- %s voto
+ - %s votos
- %s votos
cerrada
@@ -388,7 +403,7 @@
Etiqueta sin #
Limpiar
Filtro
- Componer toot
+ Escribir publicación
Redactar
¿Estás seguro de que quieres eliminar permanentemente todas tus notificaciones\?
Acciones para la imagen %s
@@ -419,7 +434,7 @@
El reporte será enviado a un moderador de tu servidor. Puedes añadir una explicación de por qué estás reportando esta cuenta a continuación:
La cuenta es de otro servidor. ¿Enviar una copia anónima del reporte\?
Mostrar filtro de notificaciones
- Mostrar siempre toots marcados con avisos de contenido
+ Mostrar siempre publicaciones marcadas con avisos de contenido
Cuentas
Error al buscar
Añadir encuesta
@@ -463,6 +478,7 @@
Habilitar gesto de deslizar para alternar entre pestañas
- %s persona
+ - %s personas
- %s personas
Etiquetas
@@ -485,20 +501,21 @@
%s recién publicado
- No puedes cargar más de %1$d archivo multimedia adjunto.
+ - No puedes cargar más de %1$d archivos multimedia adjuntos.
- No puedes cargar más de %1$d archivos multimedia adjuntos.
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:
+ Notificaciones cuando alguien al que estoy suscrito escribe una publicación
+ Nuevas publicaciones
+ alguien al que estoy suscrito hizo una nueva publicación
+ Se ocultarán algunas informaciones que podrían afectar a tu bienestar. Esto incluye:
\n
\n- Notificaciones de favoritos, impulsos y seguidores
-\n- Conteo de favoritos e impulsos en toots
-\n- Estadísticas de seguidores e toots en perfiles
+\n- Conteo de favoritos e impulsos en publicaciones
+\n- Estadísticas de seguidores y publicaciones 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
@@ -519,4 +536,54 @@
Darse de baja
Eliminar conversación
Mostrar diálogo de confirmación antes de marcar como favorito
+ una publicación con la que interactué se editó
+ Los archivos de video y audio no pueden pesar más de %s MB.
+ La imagen no pudo ser editada.
+ Siempre
+ Nunca
+ añadir reacción
+ alguien se registró
+ Error al seguir #%s
+ Error dejando de seguir #%s
+ Ingreso
+ Reingresa para activar notificaciones push
+ %s se registró
+ %s editó su publicación
+ Descartar
+ Detalles
+ Fallo cargando los detalles de la cuenta
+ Fallo cargando la página de ingreso.
+ ¿Eliminar publicación programada\?
+ Toca o arrastra el círculo para centrar el foco de la imagen, que será visible en las miniaturas.
+ ¿Guardar este borrador\? (Los adjuntos se subirán de nuevo cuando vuelvas a él.)
+ Mostrar nombre de usuario en la barra de herramientas
+ Se unió %1$s
+ 14 días
+ 365 días
+ Inicia sesión de nuevo en todas las cuentas para activar las notificaciones push.
+ Para poder usar las notificaciones push con UnifiedPush, Tusky necesita permiso para suscribirse a las notificaciones de tu servidor de Mastodon. Es necesario volver a acceder para cambiar los parámetros OAuth concedidos a Tusky. Usar aquí, o en las Preferencias de la Cuenta, la opción de volver a acceder conservarás los borradores y la caché.
+ Fallo al fijar
+ Fallo al quitarlo
+ Cuando hay varias cuentas activas
+ %s (🔗 %s)
+ Notificaciones de nuevos usuarios
+ Ediciones de una publicación
+ Notificaciones cuando se editan publicaciones con las que has interactuado
+ 1+
+ %s (%s)
+ Fallo al establecer foco
+ Establece el foco
+ Idioma de publicación
+ 30 días
+ 60 días
+ 90 días
+ 180 días
+ (Sin cambios)
+ Escribir publicación
+ Guardando borrador…
+ Has vuelto a iniciar sesión en esta cuenta para dar permiso de notificaciones push a Tusky. Sin embargo, aún hay otras cuentas que no tienen este permiso. Cambia a estas cuentas y vuelve a iniciar sesión, una a una, para activar el soporte de notificaciones de UnifiedPush.
+ Al iniciar sesión aceptas las normas de %s.
+ Normas de %s
+ Creación de cuentas
+ Editar imagen
diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml
index 9c8ff75c2..3b91e24ae 100644
--- a/app/src/main/res/values-fa/strings.xml
+++ b/app/src/main/res/values-fa/strings.xml
@@ -227,9 +227,11 @@
افزودن حساب ماستودون جدید
فهرستها
فهرستها
- در حال فرستادن با حساب %1$s
+ فرستادن از طرف %1$s
شکست در تنظیم عنوان
+ - توصیف برای کمبینایان
+\n(کران ۱ نویسه)
- توصیف برای کمبینایان
\n(کران %d نویسه)
@@ -340,7 +342,7 @@
نگارش ۴٫۰ CC-BY
نگارش ۴٫۰ CC-BY-SA
- - %1$s برگزیدن
+ - ۱ برگزیدن
- %1$s برگزیدن
@@ -541,4 +543,27 @@
رد کردن
جزییات
ذخیرهٔ پیشنویس…
+ خطا در پیگیری #%s
+ خطا در ناپیگیری #%s
+ حذف این فرستهٔ زمانبسته؟
+ قواعد %s
+ با ورودتان، قواعد %s را میپذیرید.
+ %s (%s)
+ شکست در سنجاق کردن
+ شکست در برداشتن سنجاق
+ پروندههای صوتی و ویدیویی نمیتوانند بیش از %sمب باشند.
+ تصویر نتوانست ویرایش شود.
+ زبان فرسته
+ همیشه
+ هنگام ورود چندین حساب
+ هرگز
+ نمایش نام کاربری در نوارابزارها
+ %s (🔗 %s)
+ افزودن واکنش
+ شکست در تنظیم نقطهٔ تمرکز
+ تنظیم نقطهٔ تمرکز
+ (بدون تغییر)
+ شکست در بار کردن جزییات حساب
+ ضربه زده یا دایره را کشیده تا نقطهٔ کانونیای که همواره باید در بندانگشتیها نمایان باشد را برگزینید.
+ ذخیرهٔ پیشنویس؟ (پیوستها هنگام بازگردانی پیشنویس، دوباره بارگذاری خواهند شد)
diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml
index 2cf050f7f..a9d21407b 100644
--- a/app/src/main/res/values-fi/strings.xml
+++ b/app/src/main/res/values-fi/strings.xml
@@ -184,7 +184,7 @@
Tällaista tiedostoa ei voida ladata ylös.
%s haluaa seurata sinua
Verkostoitu
- Lähetys epäonnistui.
+ Lähettäminen epäonnistui.
Ilmianna @%s
Lisähuomautuksia\?
Tilitietojen lataaminen epäonnistui
@@ -203,7 +203,7 @@
Ladataan kuvaa %1$s
Kuvauksen lisääminen epäonnistui
Mykistä keskustelu
- On syntynyt virhe.
+ Tapahtui virhe.
Halutako varmasti kirjautua ulos tililtä %s1\?
Poista jako
Luo lista
@@ -218,7 +218,7 @@
- Kuvaa näkövammaisille
\n(enintään %d merkkiä)
-
+
Aihetunniste ilman #-merkkiä
Piilotetus verkkonimet
@@ -230,9 +230,9 @@
Vastaa nopeasti
Piilota jaetut julkaisut
Täällä ei ole mitään. Liu\'uta alaspäin päivittääksesi!
- On syntynyt verkostovirhe! Tarkista yhteytesi ja yritä uudelleen!
+ Verkkovirhe! Tarkista yhteytesi ja yritä uudelleen!
Avaa media No. %d
- Julkaisun lähetys epäonnistui.
+ Julkaisun lähettäminen epäonnistui.
Tätä kenttää ei voi jättää tyhjäksi.
Tiedotukset
Ilmoitukset seuraamiesi uusista julkaisuista
@@ -268,4 +268,47 @@
Mykistä ilmoitukset tililtä %s
Yksityiskohdat
Kuvaa ei voitu muokata.
+ Listaamaton
+ Poista tämä keskustelu\?
+ Lataa median esikatselu
+ Listaamaton
+ Näytä aina arkaluonteinen sisältö
+ Lähettäminen peruutettu
+ Seuraajat
+ Aina
+ Ei koskaan
+ Buustaukset
+ Suosikit
+ Äänestykset
+ Ilmoitukset päättyneistä äänestyksistä
+ Muokkaa suodatinta
+ lisää reaktio
+ Lähetetty!
+ Lähetetty!
+ Vastaus lähetetty onnistuneesti.
+ Yhdistetään…
+ Hiljennä @%s\?
+ Piilota ilmoitukset
+ Ulkoasu
+ Näytä buustaukset
+ Merkitse media aina arkaluontoiseksi
+ Asetusten synkronointi epäonnistui
+ Pienin
+ Pieni
+ Keskikokoinen
+ Suuri
+ Suurin
+ Jaa linkki postaukseen
+ Liitteet
+ Media
+ lataa lisää
+ Julkiset aikajanat
+ Keskustelut
+ Lisää suodatin
+ Myöhemmin
+ Sisältövaroitus: %s
+ HTTP-välityspalvelin
+ Käynnistä uudelleen
+ Tunnistautuminen valitsemasi instanssin kanssa epäonnistui.
+ Estä @%s\?
\ No newline at end of file
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index efc60e461..7b2e044df 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -561,4 +561,6 @@
Langue du message
(Aucune modification)
%s (🔗 %s)
+ ajouter une réaction
+ %s règles
diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml
index 3744480a4..09deea906 100644
--- a/app/src/main/res/values-ga/strings.xml
+++ b/app/src/main/res/values-ga/strings.xml
@@ -278,7 +278,7 @@
Leathnaigh i gcónaí postálacha atá marcáilte le rabhaidh ábhair
Meáin
Ag freagairt do @%s
- luchtú níos mó
+ Lódáil a thuilleadh
Comhráite
Cuir scagaire leis
Cuir scagaire in eagar
diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml
index e804da38b..2c2727a8e 100644
--- a/app/src/main/res/values-gd/strings.xml
+++ b/app/src/main/res/values-gd/strings.xml
@@ -302,7 +302,7 @@
Dì-mhùch %s
Tagaichean hais
Luchd-leantainn
- Neo-liostaichte
+ Falaichte o liostaichean
Poblach
%1$s ’s %2$s
Thoir air falbh
@@ -578,4 +578,11 @@
%s (🔗 %s)
Dh’fhàillig suidheachadh na puinge-fòcais
Suidhich puing an fhòcais
+ A bheil thu airson am post sgeidealaichte seo a sguabadh às\?
+ Le clàradh a-steach, bidh tu ag aontachadh ri riaghailtean %s.
+ riaghailtean %s
+ cuir freagairt ris
+ Dh’fhàillig leis a’ phrìneachadh
+ Dh’fhàillig leis an dì-phrìneachadh
+ A bheil thu airson a shàbhaladh ’na dhreachd\? (Thèid na ceanglachain a luchdadh suas a-rithist nuair a dh’aisigeas tu an dreuchd.)
\ No newline at end of file
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index 5954adab2..64deaaf6a 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -257,7 +257,9 @@
Eliminar
Escribir descrición
- - Describe para persoas con problemas de visión
+
- Describe para persoas con deficiencias visuais
+\n(límite %d caracter)
+ - Describe para persoas con deficiencias visuais
\n(%d caracteres como máximo)
Fallou establecemento do texto
@@ -546,4 +548,15 @@
Establece foco
Erro ao seguir #%s
Error ao retirar seguimento de #%s
+ Normas de %s
+ Ao iniciar sesión aceptas as normas de %s.
+ engadir reacción
+ Gardar borrador\? (Os adxuntos serán subidos outra vez cando restablezas o borrador.)
+ Mostrar identificador na barra ferramentas
+ Eliminar publicación programada\?
+ Fallo ao Fixar
+ Fallo ao Desafixar
+ Sempre
+ Cando hai máis dunha conta activa
+ Nunca
\ 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 2c40293ec..bbd963146 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -561,4 +561,10 @@
Fókuszpont beállítása
Hiba a #%s követésekor
Hiba a #%s követésének befejezésekor
+ reakció hozzáadása
+ A bejelentkezéssel elfogadod a %s szabályait.
+ %s szabályai
+ Elmentsük a vázlatot\? (A mellékleteket újra feltöltjük, amikor a vázlatot visszaállítod.)
+ Nem sikerült Kitűzni
+ Nem sikerült a Kitűzés Visszavonása
diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml
index be7fc95ee..ea4b938dd 100644
--- a/app/src/main/res/values-in/strings.xml
+++ b/app/src/main/res/values-in/strings.xml
@@ -168,4 +168,84 @@
Nanti
Autentikasi gagal dilakukan.
Tulis Postingan
+ Gambar dan video tidak dapat disematkan ke dalam post yang sama.
+ Federasi
+ Login ulang untuk notifikasi push
+ Permintaan mengikuti
+ %s ter-boost
+ Meluaskan
+ Tidak ada apapun di sini. Tarik ke bawah untuk menyegarkan!
+ %s telah meng-boost post mu
+ %s memfavoritkan post mu
+ %s mendaftar
+ %s baru saja memosting
+ %s mengedit post mereka
+ Komentar tambahan\?
+ Balas
+ Balas Cepat
+ Boost
+ Hapus boost
+ Favorit
+ Hapus favorit
+ Hapus bookmark
+ Lebih
+ Menyusun
+ Masuk dengan Mastodon
+ Keluar
+ Apakah kamu yakin ingin keluar dari akun %1$s\?
+ Sembunyikan boost
+ Tampilkan boost
+ Domain tersembunyi
+ Permintaan mengikuti
+ Tambah pemilihan
+ Sebut
+ Sembunyikan media
+ Buka laci
+ Hapus dan draf ulang
+ Bagikan sebagai…
+ Bagikan URL post kepada…
+ Visibilitas post
+ Bagikan post kepada…
+ Peringatan konten
+ Nama tampilan
+ Peringatan konten
+ Papan ketik Emoji
+ Tambah Tab
+ Hashtag
+ Buka penulis boost
+ Tampilkan boost
+ Tampilkan favorit
+ Menolak
+ Hashtag
+ Tautan
+ Buka media #%d
+ Buka sebagai %s
+ Bagikan media kepada…
+ Pengguna ter-unblock
+ Instansi yang mana\?
+ Bio
+ Tidak ada hasil
+ Apa itu instansi\?
+ Unduh
+ Tarik kembali permintaan mengikuti\?
+ Sembunyikan keseluruhan domain
+ Publik: Post untuk linimasa publik
+ Tak terdaftar: Jangan tampilkan di linimasa publik
+ Hanya pengikut: Post hanya untuk pengikut
+ Langsung: Post kepada pengguna yang disebut saja
+ Pemberitahuan
+ Pemberitahuan
+ Peringatan
+ tambah reaksi
+ Unfollow akun ini\?
+ post ku telah difavoritkan
+ Hapus post ini\?
+ Hapus dan draf ulang post ini\?
+ Beritahu saya ketika
+ disebut
+ diikuti
+ post ku telah di-boost
+ pemilihan telah berakhir
+ seseorang yang saya langganani menerbitkan sebuah post baru
+ seseorang mendaftar
\ 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 a76f0b5a6..b19dcd1e7 100644
--- a/app/src/main/res/values-is/strings.xml
+++ b/app/src/main/res/values-is/strings.xml
@@ -47,7 +47,7 @@
Falin lén
Fylgjendabeiðnir
Breyta notandasniðinu þínu
- Áætluð tíst
+ Áætlaðar færslur
Notkunarleyfi
\@%s
%s endurbirti
@@ -85,8 +85,8 @@
Breyta
Eyða
Eyða og endurvinna drög
- TÍST
- TÍST!
+ BIRTA
+ BIRTA!
Reyna aftur
Loka
Notandasnið
@@ -114,11 +114,11 @@
Samþykkja
Hafna
Drög
- Áætluð tíst
+ Áætlaðar færslur
Sýnileiki færslu
Aðvörun vegna efnis
Lyklaborð með tjáningartáknum
- Tímasetja tíst
+ Tímasetja færslu
Frumstilla
Bæta við flipa
Tenglar
@@ -137,8 +137,8 @@
Deila sem …
Sækja myndefni
Næ í myndefni
- Deila slóð á tíst til…
- Deila tísti með…
+ Deila slóð á færslu til…
+ Deila færslu með…
Deila myndefni með…
Sent!
Hætt að útiloka notanda
@@ -169,8 +169,8 @@
Sækja
Afturkalla beiðni um að fylgjast með\?
Hætta að fylgjast með þessum aðgangi\?
- Eyða þessu tísti\?
- Eyða og endurvinna þetta tíst\?
+ Eyða þessari færslu\?
+ Eyða og endurvinna þessa færslu\?
Ertu alveg algjörlega viss um að þú viljir loka á allt %s\? Þú munt ekki sjá efni frá þessu léni í neinum opinberum tímalínum eða í tilkynningunum þínum. Fylgjendur þínir frá þessu léni verða fjarlægðir.
Fela allt lénið
Opinbert: Senda á opinberar tímalínur
@@ -254,8 +254,8 @@
Villutilkynningar og beiðnir um nýja eiginleika:
\n https://github.com/tuskyapp/Tusky/issues
Notandasnið Tusky
- Deila efni úr tísti
- Deila tengli á tíst
+ Deila efni úr færslu
+ Deila tengli á færslu
Myndir
Myndskeið
Beðið um að fylgja
@@ -296,9 +296,11 @@
Leita að fólki sem þú fylgist með
Bæta notandaaðgangi á listann
Fjarlægja notandaaðganginn af listanum
- Sendi með notandaaðgangnum %1$s
+ Sendi sem %1$s
Ekki tókst að setja skýringatexta
+ - Lýstu þessu fyrir sjónskerta
+\n(hámark %d stafur)
- Lýstu þessu fyrir sjónskerta
\n(hámark %d stafir)
@@ -307,11 +309,11 @@
Læsa notandaaðgangi
Krefst þess að þú samþykkir fylgjendur handvirkt
Vista drög\?
- Sendi tíst…
- Villa við að senda tíst
- Sendi tíst
+ Sendi færslu…
+ Villa við að senda færslu
+ Sendi færslur
Aflýsti sendingu
- Afrit af tístinu þínu hefur verið vistað drögunum þínum
+ Afrit af færslunni hefur verið vistað í drögunum þínum
Semja skilaboð
Tilvikið þitt %s er ekki með nein sérsniðin tjáningartákn
Stíll tjáningartákna
@@ -319,7 +321,7 @@
Þú þarft fyrst að ná í þessi táknmyndasett
Framkvæmi uppflettingu…
Þenja út / Fella saman allar stöðufærslur
- Opna tíst
+ Opna færslu
Endurræsing forrits er nauðsynleg
Það þarf að endurræsa Tusky til að breytingarnar taki gildi
Seinna
@@ -344,7 +346,7 @@
Nota algildan tíma
Ekki er víst að upplýsingarnar hér að neðan endurspegli notandasniðið að fullu. Opnaðu fullt notandasnið í vafra.
Losa
- Pin
+ Festa
Endurbirt af
Sett í eftirlæti af
%1$s
@@ -480,10 +482,10 @@
Segja upp áskrift
Gerast áskrifandi
Hreyfa sérsniðin tjáningartákn
- Tístið sem þú gerðir drög að svari við hefur veriið fjarlægt
+ Færslan sem þú gerðir drög að svari við hefur verið fjarlægð
Eyddi drögum
Mistókst að hlaða inn svarupplýsingum
- Mistókst að senda þetta tíst!
+ Mistókst að senda þessa færslu!
Viðhengi
Hljóð
Ertu viss um að þú viljir eyða %s listanum\?
@@ -542,4 +544,19 @@
Villa við að hætta að fylgjast með #%s
Mistókst að hlaða inn nánari upplýsingum notandaaðgangs
Ekki var hægt að breyta myndinni.
+ Eyða þessari áætluðu færslu\?
+ Reglur %s
+ Með því að skrá þig inn samþykkir þú reglurnar á %s.
+ bæta við viðbrögðum
+ Ýttu eða dragðu hringinn til að setja virknistað sem verður ævinlega sýnilegur í smámyndum.
+ Vista drög\? (Viðhengi verða send inn aftur þegar þú endurheimtir drögin.)
+ Mistókst að festa
+ Mistókst að losa
+ Alltaf
+ Þegar er skráð inn á mörgum aðgöngum
+ %s (🔗 %s)
+ Mistókst að setja virknistað
+ Setja virknistað
+ Aldrei
+ Birta notandanafn á verkfærastikum
\ No newline at end of file
diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml
index 707ce4211..b584e31a1 100644
--- a/app/src/main/res/values-it/strings.xml
+++ b/app/src/main/res/values-it/strings.xml
@@ -328,7 +328,7 @@
Contenuto
Usa ora assoluta
Il profilo dell\'utente mostrato qui sotto potrebbe essere incompleto. Premi per aprire il profilo completo nel browser.
- Smetti di fissare
+ Non fissare in cima
Fissa
- %1$s Preferito
diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml
index 620c65703..c1a1f9ded 100644
--- a/app/src/main/res/values-ja/strings.xml
+++ b/app/src/main/res/values-ja/strings.xml
@@ -241,6 +241,7 @@
動画
フォローリクエスト中
+ 現在
%d年後
%d日後
%d時間後
diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml
index a8efb2117..6ee623625 100644
--- a/app/src/main/res/values-nb-rNO/strings.xml
+++ b/app/src/main/res/values-nb-rNO/strings.xml
@@ -4,7 +4,7 @@
En nettverksfeil har oppstått! Sjekk tilkoblingen, og prøv igjen!
Denne kan ikke være tom.
Ugyldig domene
- Autentisering feilet.
+ Kunne ikke autentisere med den instansen.
Fant ingen nettleser som kunne brukes.
En ukjent autoriseringsfeil oppsto.
Autorisasjon ble nektet.
@@ -14,13 +14,13 @@
Den filen kunne ikke åpnes.
Trenger tillatelse til å lese media.
Trenger tillatelse for å lagre media.
- Bilder og videoer kan ikke kobles til samme innlegg.
+ Bilder og videoer kan ikke legges til samme innlegg.
Opplastingen feilet.
En feil oppsto under sending av innlegget.
Hjem
Varsler
Lokal
- Forent
+ Føderert
Direktemeldinger
Faner
Tråd
@@ -34,28 +34,28 @@
Blokkerte brukere
Følgeforespørsler
Endre profilen din
- Kladder
+ Utkast
Lisenser
\@%s
- %s fremhevet
+ %s delte
Sensitivt innhold
Media skjult
- Klikk for å vise
+ Trykk for å vise
Vis mer
Vis mindre
Utvid
- Kollaps
- Her er det ingenting.
- Her er det ingenting. Dra ned for å oppdatere!
- %s fremhevde innlegget ditt
+ Skjul
+ Ingenting her.
+ Ingenting her. Dra ned for å oppdatere!
+ %s delte innlegget ditt
%s favoriserte innlegget ditt
%s følger deg
Rapporter @%s
Ytterligere kommentarer\?
Hurtigsvar
Svar
- Fremhev
- Fjern fremheving
+ Del
+ Fjern deling
Legg til i favoritter
Fjern favoritt
Mer
@@ -67,12 +67,12 @@
Slutt å følge
Blokkér
Fjern blokkering
- Skjul fremhevinger
- Vis fremhevinger
+ Skjul delinger
+ Vis delte inlegg
Rapporter
Slett
- Publiser
- Publiser!
+ TUT
+ TUT!
Prøv igjen
Steng
Profil
@@ -99,29 +99,29 @@
Aksepter
Avvis
Søk
- Kladder
+ Utkast
Synlighet på innlegg
Innholdsadvarsel
Emoji-tastatur
Legg til fane
- Linker
- Nevner
+ Lenker
+ Nevnelser
Stikkord
- Åpne innlegg-forfatter
- Vis fremhevinger
+ Åpne deler
+ Vis delinger
Vis favoritter
Stikkord
- Nevner
- Linker
+ Nevnelser
+ Lenker
Åpne media #%d
Laster ned %1$s
- Kopier link
+ Kopier lenken
Åpne som %s
Del som …
Last ned media
Laster ned media
- Del innlegg-URL til…
- Del innlegg til…
+ Del tut-URL til…
+ Del tut til…
Del media til…
Sendt!
Fjernet blokkering av bruker
@@ -140,20 +140,20 @@
Overskrift
Hva er en instans\?
Kobler til…
- more!
+ Addressen eller domenet til enhver instans kan legges til her, f. eks. mastodon.social, icosahedron.website, social.tchncs.de og mer!
\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.
+\nHvis du ikke har en konto kan du gi navnet på instansen du vil bli medlem av, og lage en konto der.
\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.
+\nEn instans er en plass der du har kontoen din, men du kan lett kommunisere med og følge andre personer på andre instanser, som om du var på den samme nettsiden.
\n
-\nMore info can be found at joinmastodon.org.
- Media opplasting er ferdig
+\nDu kan finne mer info på joinmastodon.org.
+ Opplasting av Media er ferdig
Laster opp…
Last ned
Trekk tilbake følgeforespørselen\?
- Slutte å følge denne kontoen\?
- Slette dette innlegget\?
- Offentlig: Vis i offentlig tidslinjer
+ Slutt å følge denne kontoen\?
+ Slett dette innlegget\?
+ Offentlig: Vis i offentlige tidslinjer
Ikke oppført: Ikke vis i offentlige tidslinjer
Bare følgere: Vis bare til følgere
Direkte: Vis bare til nevnte brukere
@@ -164,21 +164,21 @@
Varsle med vibrasjon
Varsle med lys
Varsle meg når
- nevnt
- fulgt
- innleggene mine blir fremhevet
+ jeg blir nevnt
+ jeg blir fulgt
+ innleggene mine blir delt
innleggene mine blir favorisert
Utseende
Apptema
Tidslinjer
Filtere
Nettleser
- Bruke Chrome tilpassede faner
- Skjul skriv-knappen under scrolling
+ Bruke Chrome-tilpassede faner
+ Skjul skriv-knappen ved scrolling
Språk
Tidslinjefiltrering
Faner
- Vis fremhevinger
+ Vis delinger
Vis svar
Last ned forhåndsvisning av media
Proxy
@@ -192,12 +192,12 @@
Størrelse på statustekst
Nye følgere
Varsler om nye følgere
- Fremhevinger
- Varsler når innleggene dine blir fremhevet
+ Delte innlegg
+ Varsler når innleggene dine blir delt
Favoritter
Varsler når innleggene dine blir favorisert
%s nevnte deg
- %1$s, %2$s, %3$s og %4$d anre
+ %1$s, %2$s, %3$s og %4$d andre
%1$s, %2$s, og %3$s
%1$s og %2$s
@@ -209,9 +209,9 @@
Yuito %s
Rapporter feil og ønsker om funksjonalitet her:
\n https://github.com/accelforce/Yuito/issues
- Yuitos Mastodon-profil
+ Yuitos Profil
Del inneholdet i innlegget
- Del link til innlegget
+ Del lenke til tuten
Bilder
Video
Forespørsel sendt
@@ -233,9 +233,9 @@
Samtaler
Legg til filter
Endre filter
- Slett
+ Fjern
Oppdater
- Frase å filtere
+ Filtrer frase
Legg til konto
Legg til ny Mastodon-konto
Lister
@@ -245,19 +245,19 @@
Kunne ikke slette liste
Opprett en liste
Gi listen nytt navn
- Slett listen
+ Fjern listen
Endre listen
Søk etter personer du følger
Legg til konto i listen
- Slett konto fra listen
- Standard proxy-personvern
- Nye omtaler
- Varsler om nye omtaler
+ Fjern konto fra listen
+ Standardinstilling for innlegg
+ Nye nevnelser
+ Varsler om nye nevnelser
Yuito er fri og åpen kildekode. Applikasjonen er lisensiert under GNU General Public License versjon 3. Du kan se lisensen her: https://www.gnu.org/licenses/gpl-3.0.en.html
Hjemmeside:
\n https://accelf.net/yuito
om %dy
- Poster som konto %1$s
+ Poster som %1$s
Klarte ikke å sette bildetekst
- Beskriv for de med nedsatt synsevne
@@ -269,32 +269,32 @@
Slett
Lås konto
Krever at du manuelt godkjenner nye følgere
- Lagre kladd\?
+ Lagre utkast\?
Sender innlegg…
Det oppsto en feil under sending av innlegget
Sender innleggene
Sending avbrutt
- En kopi av innlegget er lagret i kladdene dine
+ En kopi av innlegget er lagret i utkastene dine
Skriv
Instansen %s har ingen egendefinerte emojis
Emoji-stil
Systemstandard
Du må laste ned emoji-samlingene før de kan brukes
- Gjennomfører oppslag…
- Utvid/kollaps alle statuser
- Åpne innlegg
- Omstart av applikasjonen er påkrevd
- Du må starte Yuito på nytt for at endringene skal bli aktive
+ Søker…
+ Utvid/Gjem alle statuser
+ Åpne tut
+ Omstart av applikasjonen kreves
+ Du må starte Yuito på nytt for at endringene blir aktivert
Senere
Start på nytt
- Din enhets standard emoji-samling
+ Standard-emojis for din enhet
Blob-emojis kjent fra Android 4.4–7.1
Mastadons standard emoji-samling
Nedlasting feilet
Robot
%1$s har flyttet til:
- Fremhev til opprinnelig publikum
- Fjern fremheving
+ Del til opprinnelig publikum
+ Fjern deling
Yuito inneholder programkode og elementer fra følgende åpen kildekode-prosjekter:
Lisensiert under Apache License (kopi under)
CC-BY 4.0
@@ -312,10 +312,10 @@
- <b>%1$s</b> Favoritter
- - %s Fremheving
- - %s Fremhevinger
+ - Delt %s gang
+ - Delt %s ganger
- Fremhevet av
+ Delt av
Favorisert av
%1$s
%1$s og %2$s
@@ -340,10 +340,10 @@
Bruk
Skriv innlegg
Skriv
- Vis at konto er en robot
+ Vis robotindikator
Er du sikker på at du vil slette alle varsler\?
Slett og skriv på nytt
- Vil du slette dette tottet og skrive det på nytt\?
+ Vil du slette denne tuten og skrive den på nytt\?
%1$s • %2$s
- %s stemme
@@ -404,11 +404,11 @@
Skjulte domener
Demp %s
%s er ikke lenger skjult
- Er du sikker på at du vil blokkere hele %s\? Du kommer ikke til å se innhold fra domenet i noen offentlige tidslinjer, eller i varslene dine. Kontoer som følger deg fra domenet vil bli fjernet.
+ Er du sikker på at du vil blokkere alt fra %s\? Du kommer ikke til å se innhold fra domenet i noen offentlige tidslinjer, eller i varslene dine. Kontoer som følger deg fra dette domenet vil bli fjernet.
Skjul hele domenet
Vis varselfilter
- Hele ordet
- Når nøkkelordet kun inneholder bokstaver og tall, vil det bare brukes dersom det stemmer overens med hele ordet
+ Helt ord
+ Når nøkkelordet eller frasen kun inneholder bokstaver og tall, vil det bare brukes dersom det stemmer overens med hele ordet
Kontoer
Klarte ikke å søke
Ekspander alltid innlegg markert med innholdsadvarsel
@@ -439,10 +439,10 @@
Velg liste
Liste
Du har ingen planlagte innlegg.
- Du har ikke lagret noen kladder.
+ Du har ikke lagret noen utkast.
Mastodon har et minimums planleggingsinterval på 5 minutter.
Vis forhåndsvisning av linker i tidslinjer
- Vis bekreftelsesdialog før fremheving
+ Vis bekreftelsesdialog før deling
Skru på sveiping for å bytte mellom faner
- %s person
@@ -478,8 +478,8 @@
Se over varsler
Informasjon som kan påvirke ditt mentale velvære vil bli skjult. Dette inkluderer:
\n
-\n - Varsler om favorisering, fremhevinger og følgere
-\n - Antall favoriseringer og boots på innlegg
+\n - Varsler om favorisering, deling og følgere
+\n - Antall favoriseringer og delinger av innlegg
\n - Antall følgere og innlegg på profiler
\n
\n Push-varsler vil ikke påvirkes, men du kan se over dine varselinnstillinger manuelt.
@@ -497,17 +497,17 @@
Er du sikker på at du vil slette listen %s\?
Vedlegg
Lyd
- Innlegget du kladdet et svar på har blitt fjernet
- Kladd slettet
+ Innlegget du hadde opprettet et utkast som svar på har blitt fjernet
+ Utkast slettet
Lasting av svarinformasjon feilet
Sending av innlegg feilet!
Animer egendefinerte emojis
Avslutt abonnementet
Abonner
Selv om kontoen din ikke er låst, har %1$s administratorer markert disse følgeforespørsler for manuell godkjenning.
- Slette denne samtalen\?
+ Slett denne samtalen\?
Slett samtale
- Slett bokmerke
+ Fjern bokmerke
Vis bekreftelsesdialog når favoritt skal legges til
30 dager
60 dager
@@ -521,32 +521,32 @@
Registreringer
Varslinger om nye brukere
%s redigerte innlegget sitt
- et innlegg jeg har hatt en interaksjon med er redigert
+ et innlegg jeg har interagert med, er redigert
Redigerte innlegg
- Varslinger når et innlegg du har hatt en interaksjon med er redigert
- Innlogging
+ Varslinger når et innlegg du har interagert med er redigert
+ Logg inn
Klarte ikke å laste innloggingssiden.
Logg inn på nytt for pushvarsler
Avvis
Detaljer
Ble med %1$s
Logg inn all konti på nytt for å skru på pushvarsler.
- For å kunne sende pushvarsler via UnifiedPush trenger Tusky tillatelse til å abonnere på varsler på Mastodon-serveren. Dette krever at du logger inn på nytt. Ved å bruke muligheten til å logge inn på nytt her eller i kontoinstillinger vil alle lokale kladder være tilgjengelig også etter at du har logget inn på nytt.
+ For å kunne sende pushvarsler via UnifiedPush trenger Tusky tillatelse til å abonnere på varsler på Mastodon-serveren. Dette krever at du logger inn på nytt. Ved å bruke muligheten til å logge inn på nytt her eller i kontoinstillinger vil alle lokale utkast være tilgjengelig også etter at du har logget inn på nytt.
Du har logget inn på nytt for å tillate Tusky til å sende pushvarsler, men du har fortsatt andre konti som ikke har fått den nødvendige tillatelsen. Bytt til dem og logg inn på nytt på samme måte for å skru på støtte for pushvarsler via UnifiedPush.
- Lagrer kladd…
+ Lagrer utkast…
1+
Rediger bilde
Bildet kunne ikke redigeres.
Lasting av kontodetaljer feilet
Video- og lydfiler kan ikke være større enn %s MB.
Det oppsto en feil under følging av #%s
- Det oppsto en feil når følging av #%s skulle avsluttes
+ Kunne ikke slutte å følge #%s
%s (%s)
(Ingen endring)
Innleggspråk
%s (🔗 %s)
Sett fokuspunkt
- Klarte ikke å sette et fokuspunkt
+ Klarte ikke å sette fokuspunkt
Trykk eller dra sirkelen for å velge fokuspunktet som alltid skal være synlig i miniatyrbilder.
Alltid
Når flere konti er logget inn
@@ -555,4 +555,8 @@
Slette dette planlagte innlegget\?
Regler på %s
Ved å logge inn godtar du reglene på %s.
+ Lagre utkast\? (Vedlegg vil bli lastet opp igjen når du fortsetter å jobbe på utkastet.)
+ Klarte ikke å feste
+ Klarte ikke å løsne
+ Legg til reaksjon
diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml
index 544117b30..7e1012da1 100644
--- a/app/src/main/res/values-pt-rPT/strings.xml
+++ b/app/src/main/res/values-pt-rPT/strings.xml
@@ -566,4 +566,5 @@
Define o ponto de focagem
Erro ao seguir #%s
Erro ao deixar de seguir #%s
+ adicionar reação
\ No newline at end of file
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index 9e27c6837..bf992f5ee 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -9,21 +9,21 @@
Ett oidentifierat behörighetsfel inträffade.
Ingen behörighet.
Misslyckades med att få en inloggnings-token.
- Statusen är för lång!
+ Inlägget är för långt!
Den typen av fil kan inte laddas upp.
Den filen kunde inte öppnas.
Behörighet att läsa media krävs.
Behörighet att spara media krävs.
- Bilder och videoklipp kan inte båda bifogas i samma status.
+ Bilder och videoklipp kan inte båda bifogas i samma inlägg.
Uppladdningen misslyckades.
- Kunde inte skicka toot.
+ Kunde inte posta inlägg.
Hem
Aviseringar
Lokalt
Federerat
Direkta meddelanden
Flikar
- Toot
+ Tråd
Inlägg
Med svar
Fastnålade
@@ -47,8 +47,8 @@
Dölj
Ingenting här.
Inget här. Dra ner för att uppdatera!
- %s knuffade din toot
- %s favoriserade din toot
+ %s puffade ditt inlägg
+ %s favoriserade ditt inlägg
%s följer dig
Rapportera @%s
Ytterligare kommentarer?
@@ -100,7 +100,7 @@
Avvisa
Sök
Utkast
- Toot synlighet
+ Inläggssynlighet
Innehållsvarning
Emoji-tangentbord
Lägg till flik
@@ -208,14 +208,15 @@
Nya följare
Aviseringar på nya följare
Knuffar
- Aviseringar när dina toots blir knuffade
+ Aviseringar när dina inlägg blir puffade
Favoriter
- Aviseringar när dina toots blir markerade som favoriter
+ Aviseringar när dina inlägg blir favoritmarkerade
%s omnämnde dig
%1$s, %2$s, %3$s och %4$d andra
%1$s, %2$s, och %3$s
%1$s och %2$s
+ - %d ny interaktion
- %d nya interaktioner
Låst konto
@@ -277,10 +278,13 @@
Sök efter personer du följer
Lägg till konto i listan
Ta bort kontot från listan
- Inlägg med kontot %1$s
+ Publicerar som %1$s
Misslyckades med att ange bildtext
- - Beskriv för synskadade\n(%d teckengräns)
+ - Beskriv för synskadade
+\n(max %d tecken)
+ - Beskriv för synskadade
+\n(max %d tecken)
Ange bildtext
Ta bort
@@ -338,10 +342,10 @@
%1$s och %2$s
%1$s, %2$s och %3$d mer
- - max antal flikar %1$d uppnådd
+ - max antal flikar %1$d uppnådd
+ - max antal flikar %1$d uppnådda
- Media: %s
-
+ Media: %s
Innehållsvarning: %s
Ingen beskrivning
@@ -361,7 +365,7 @@
Ladda ned media
Laddar ned media
Hashtag utan #
- Skriv toot
+ Skriv inlägg
Skriv
Rensa
Filtrera
@@ -422,7 +426,7 @@
Visa notifikationsfilter
Helt ord
När nyckelordet eller frasen enbart är alfanumerisk, appliceras den om den matchar hela ordet
- Expandera alltid toots med innehållsvarningar
+ Expandera alltid inlägg med innehållsvarningar
Konton
Sökning misslyckades
Skapa en omröstning
@@ -485,9 +489,9 @@
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
+ Aviseringar när någon du följer skrivit ett nytt inlägg
+ Nya inlägg
+ någon jag följer har skrivit ett nytt inlägg
%s skrev precis
Dölj kvantitativ information på profiler
Dölj kvantitativ information på inlägg
@@ -495,10 +499,81 @@
Ä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- Favoritmarkering-/Knuff-/Följaraviseringar
+\n- Favoritmarkering/Antal knuffar på inlägg
+\n- Följare/Inläggsstatistik på profiler
\n
\nPush-aviseringar påverkas inte, men du ändra dina aviseringinställningar manuellt.
Välmående
+
+ - Du kan inte ladda upp fler än %1$d mediebilaga.
+ - Du kan inte ladda upp fler än %1$d mediebilagor.
+
+ Radera detta schemalagda inlägg\?
+ Genom att logga in accepterar du reglerna på %s.
+ %s regler
+ Kunde inte avfölja #%s
+ Radera denna konversation\?
+ Alltid
+ För att använda pushnotiser via UnifiedPush behöver Tusky din tillåtelse att prenumerera på notiser på din Mastodon-server. Detta kräver att du loggar in igen för att ändra vilka OAuth-scopes Tusky har tillgång till. Genom använda alternativet logga in igen här eller i Kontoinställningarna behåller du alla dina lokala utkast och data i cache.
+ Tryck eller dra cirkeln för att välja fokuspunkten som alltid kommer synas i miniatyrbilder.
+ Varaktighet
+ Oändligt
+ Du har loggat in igen på ditt konto för att ge Tusky tillgång till push-prenumeration. Dock har du andra konton som inte har migrerats såhär ännu. Växla till dem och logga in igen för att aktivera stöd för UnifiedPush-notiser.
+ Sluta prenumerera
+ Inläggsspråk
+ %s (🔗 %s)
+ När flera konton är inloggade
+ Aldrig
+ Registreringar
+ Notiser om nya användare
+ Inläggsredigeringar
+ Notiser när inlägg du interagerat med redigerats
+ %s (%s)
+ Redigera bild
+ 14 dagar
+ 30 dagar
+ 60 dagar
+ 90 dagar
+ (Ingen ändring)
+ Visa användarnamn i verktygsrader
+ Visa bekräftelsedialog före favoritmarkering
+ Vill du verkligen radera listan %s\?
+ Det här inlägget kunde inte skickas!
+ Kunde inte ladda information om svar
+ Utkast raderat
+ Inlägget du skrev ett utkast till svar på har raderats
+ Även om ditt konto inte är låst så tänker administratörerna på %1$s att du kanske ändå vill granska följförfrågan från dessa konton manuellt.
+ Prenumerera
+ Skriv inlägg
+ Gick med i %1$s
+ Sparar utkast…
+ Logga in igen på alla konton för att tillåta pushnotiser.
+ Video- och ljudfiler kan inte överskrida %s MB i storlek.
+ Bilden kunde inte redigeras.
+ 365 dagar
+ 180 dagar
+ Kunde inte sätta fokuspunkt
+ Sätt fokuspunkt
+ Kunde inte följa #%s
+ %s registrerade sig
+ %s redigerade sitt inlägg
+ Ta bort bokmärke
+ Radera konversation
+ någon registrerade sig
+ ett inlägg jag interagerat med har redigerats
+ Animera skräddarsydda emojis
+ Ljud
+ Bilagor
+ 1+
+ Logga in igen för pushnotiser
+ Avvisa
+ Detaljer
+ Kunde inte ladda kontodetaljer
+ Logga in
+ Kunde inte ladda inloggningssidan.
+ Spara utkast\? (Bilagor kommer att laddas upp igen när du återställer utkastet.)
+ Kunde inte fästa
+ Kunde inte lossa
+ lägg till reaktion
\ No newline at end of file
diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml
index 6609963e2..41f27aa1c 100644
--- a/app/src/main/res/values-uk/strings.xml
+++ b/app/src/main/res/values-uk/strings.xml
@@ -471,7 +471,7 @@
Приховати кількісну статистику дописів
Обмеження сповіщень стрічки
Переглянути сповіщення
- Ваша особиста примітка щодо цього облікового запису
+ Ваша особиста примітка про цей обліковий запис
Добробут
Сховати заголовок верхньої панелі інструментів
Запитувати підтвердження перед просуванням
@@ -581,4 +581,8 @@
Видалити цей запланований допис\?
Увійшовши, ви погоджуєтесь з правилами %s.
Правила %s
+ Не вдалося прикріпити
+ Не вдалося відкріпити
+ Зберегти чернетку\? (Вкладення будуть завантажені знову, коли ви відновите чернетку.)
+ додати реакцію
\ 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 03bffd856..88b707a7b 100644
--- a/app/src/main/res/values-vi/strings.xml
+++ b/app/src/main/res/values-vi/strings.xml
@@ -189,7 +189,7 @@
Ghim
Trả lời
Tút
- Tút
+ Nội dung tút
Xếp tab
Nhắn riêng
Liên hợp
@@ -542,4 +542,8 @@
Bạn có chắc muốn xóa tút đã lên lịch\?
Đăng nhập nghĩa là bạn đồng ý với quy tắc của %s.
%s quy tắc
+ Lưu bản nháp\? (Bạn sẽ cần tải lên lại file đính kèm)
+ Không thể bỏ ghim
+ Không thể ghim
+ biểu cảm
\ 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 8b6b30055..0c8373b49 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -11,12 +11,12 @@
未能获取登录令牌。
嘟文太长了!
无法上传此类型的文件。
- 打不开此文件。
+ 此文件无法打开。
需要授予 Yuito 读取媒体文件的权限。
需要授予 Yuito 存储媒体的权限。
无法在一篇嘟文中同时插入视频和图片。
上传失败。
- 嘟文发送时出错。
+ 嘟文发送时发生出错。
主页
通知
本站时间轴
@@ -25,11 +25,11 @@
标签页
嘟文
嘟文
- 有回复
+ 嘟文和回复
已置顶
正在关注
关注者
- 喜欢
+ 收藏
被隐藏的用户
被屏蔽的用户
关注请求
@@ -51,7 +51,7 @@
%s 喜欢了你的嘟文
%s 关注了你
举报 @%s
- 是否有更多信息需报告?
+ 报告更多信息?
快速回复
回复
转嘟
@@ -396,7 +396,7 @@
重置
书签
- 隐藏的域名
+ 被隐藏的域名
定时嘟文
书签
编辑
@@ -509,7 +509,7 @@
新嘟文
显示动态自定义Emoji
关注的人发布了新嘟文
- %s 发送了新嘟文
+ %s 刚刚发送了新嘟文
即使您的账号未上锁,管理员 %1$s 认为您可能需要手动处理来自这些账号的关注请求。
删除此对话吗?
删除对话
@@ -561,4 +561,8 @@
删除这条定时嘟文吗?
登录即表示您同意 %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 2350c6e8a..730680e31 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -287,7 +287,8 @@
以 %1$s 發嘟文
設定圖片標題失敗
- - 為視覺障礙用戶提供的描述\n(限制 %d 字)
+ - 為視覺障礙用戶提供的描述
+\n(限制 %d 字)
設定圖片標題
移除
@@ -521,4 +522,59 @@
帳號
登入
無法載入登入頁面。
+ 確定要刪除這則排程嘟文嗎?
+ 登入既代表您已同意 %s 的規定。
+ %s 的規定
+ 確認要刪除此對話嗎?
+ %s (%s)
+ 輕按或拖動圓圈來選擇總是在縮圖中可視的關注點。
+ 嘟文語言
+ %s (🔗 %s)
+ 是否要儲存草稿?(當你重開草稿時附檔將會被再次上傳。)
+ 編寫嘟文
+ 釘選失敗
+ 取消釘選失敗
+ 在工具列顯示使用者名稱
+ 標註為喜歡前顯示確認對話框
+ 加入自 %1$s
+ 總是
+ 登入多個帳號時
+ 從不
+ 註冊
+ 新使用者通知
+ 嘟文編輯
+ 當你互動過的嘟文被編輯時發出通知
+ 雖然您的帳號未上鎖,管理者 %1$s 認為您或許需要手動處理來自這些帳號的追蹤請求。
+ 訂閱
+ 取消訂閱
+ 正在儲存草稿…
+ 重新登入所有帳號以啟用推播功能。
+ 設置關注點失敗
+ 設置關注點
+ 重新登入以啟用推播功能
+ 影片和音訊檔案大小不能超過 %s MB。
+ 1+
+ 添加反應
+ 有人進行了註冊
+ 編輯圖片
+ 30 天
+ 60 天
+ 90 天
+ 180 天
+ 365 天
+ (無更改)
+ 追蹤 #%s 時發生錯誤
+ 取消追蹤 #%s 時發生錯誤
+ 移除書籤
+ 刪除對話
+ 撤銷
+ 詳情
+ 我互動過的嘟文被編輯了
+ 14 天
+ 為了透過 UnifiedPush使用推播功能,Tusky 需要獲得訂閱您 Mastodon 服務器上的通知之權限。這會需要重新登入才能更改授予 Tusky 的 OAuth 範疇。在此頁面或帳戶設定頁面中使用重新登入選項將會保留您所有的本機草稿和快取。
+ 您已重新登入當前帳號並授予 Tusky 推送訂閱的權限。 然而,您仍擁有其他帳號未以此種方式遷移。 請切換到該帳號,並且逐一重新登入,以啟用 UnifiedPush 的通知支援。
+ 加載賬戶詳情失敗
+ %s 已註冊
+ %s 編輯了他們的嘟文
+ 這張圖片不能編輯。
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index ea2e744b6..8ec84acf9 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -58,4 +58,5 @@
16dp
+ 4dp
diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml
index f3155afdf..19cecab8f 100644
--- a/app/src/main/res/values/donottranslate.xml
+++ b/app/src/main/res/values/donottranslate.xml
@@ -11,6 +11,8 @@
#%s
:%s:
+ %s *
+ " • "
- public
@@ -90,7 +92,7 @@
- cs
- cy
- de
- - en-gb
+ - en-GB
- en
- eo
- es
@@ -103,7 +105,7 @@
- it
- hu
- nl
- - nb-no
+ - nb-NO
- oc
- pl
- pt-BR
@@ -118,8 +120,8 @@
- uk
- ar
- ckb
- - bn-bd
- - bn-in
+ - bn-BD
+ - bn-IN
- fa
- hi
- sa
@@ -151,8 +153,8 @@
-
- %1$s; %2$s; %3$s, %14$s %4$s, %5$s; %6$s, %7$s, %8$s, %9$s, %10$s; %11$s, %12$s, %13$s
+
+ %1$s; %2$s; %3$s, %15$s %4$s, %5$s, %6$s; %7$s, %8$s, %9$s, %10$s, %11$s; %12$s, %13$s, %14$s
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b7f7e0257..3f35da651 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -24,6 +24,9 @@
Error sending post.
Error following #%s
Error unfollowing #%s
+ This instance does not support following hashtags.
+ Error muting #%s
+ Error unmuting #%s
Collecting data…
Failed.
@@ -54,6 +57,7 @@
Scheduled posts
Announcements
Licenses
+ Followed Hashtags
\@%s
%s boosted
@@ -64,6 +68,7 @@
Show Less
Expand
Collapse
+ Edited %s
This toot has %d media(s).
Nothing here.
@@ -76,6 +81,9 @@
%s signed up
%s just posted
%s edited their post
+ New report on %s
+ %s reported %s
+ %s · %d posts attached
Report @%s
Additional comments?
@@ -158,6 +166,7 @@
Show favorites
Dismiss
Details
+ add reaction
Quote
Authorize Now!
Jump to top
@@ -187,6 +196,7 @@
User unblocked
User unmuted
%s unhidden
+ #%s unfollowed
Sent!
Reply sent successfully.
@@ -257,6 +267,7 @@
somebody I\'m subscribed to published a new post
somebody signed up
a post I\'ve interacted with is edited
+ there\'s a new report
Appearance
App Theme
Timelines
@@ -298,6 +309,7 @@
HTTP proxy port
Default post privacy
+ Default posting language
Always mark media as sensitive
Publishing (synced with server)
Failed to sync settings
@@ -341,6 +353,8 @@
Notifications about new users
Post edits
Notifications when posts you\'ve interacted with are edited
+ Reports
+ Notifications about moderation reports
%s mentioned you
%1$s, %2$s, %3$s and %4$d others
@@ -386,6 +400,7 @@
Audio
Attachments
1+
+ now
Follow requested
@@ -435,6 +450,9 @@
Search for people you follow
Add account to the list
Remove account from the list
+ Add or remove from list
+ Failed to add the account to the list
+ Failed to remove the account from the list
Posting as %1$s
@@ -533,6 +551,9 @@
No description
+
+ Edited
+
Reblogged
@@ -656,6 +677,7 @@
You don\'t have any drafts.
You don\'t have any scheduled posts.
There are no announcements.
+ You don\'t have any lists.
Mastodon has a minimum scheduling interval of 5 minutes.
Show username in toolbars
Show link previews in timelines
@@ -707,5 +729,12 @@
By logging in you agree to the rules of %s.
%s rules
+ %s (%s)
+
+ Rule violation
+ Spam
+ Other
+
+ Unfollow #%s?
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 70d274044..e34584989 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -98,7 +98,6 @@
+
+
diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml
new file mode 100644
index 000000000..3c2bb0072
--- /dev/null
+++ b/app/src/main/res/xml/locales_config.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt
index 9465c910e..38989b531 100644
--- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt
@@ -30,8 +30,6 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.Parameterized
import org.mockito.ArgumentMatchers.anyBoolean
import org.mockito.Mockito.eq
import org.mockito.kotlin.doReturn
@@ -71,6 +69,7 @@ class BottomSheetActivityTest {
reblog = null,
content = "omgwat",
createdAt = Date(),
+ editedAt = null,
emojis = emptyList(),
reblogsCount = 0,
favouritesCount = 0,
@@ -109,70 +108,6 @@ class BottomSheetActivityTest {
activity = FakeBottomSheetActivity(apiMock)
}
- @RunWith(Parameterized::class)
- class UrlMatchingTests(private val url: String, private val expectedResult: Boolean) {
- companion object {
- @Parameterized.Parameters(name = "match_{0}")
- @JvmStatic
- fun data(): Iterable {
- return listOf(
- arrayOf("https://mastodon.foo.bar/@User", true),
- arrayOf("http://mastodon.foo.bar/@abc123", true),
- arrayOf("https://mastodon.foo.bar/@user/345667890345678", true),
- arrayOf("https://mastodon.foo.bar/@user/3", true),
- arrayOf("https://pleroma.foo.bar/users/meh3223", true),
- arrayOf("https://pleroma.foo.bar/users/meh3223_bruh", true),
- arrayOf("https://pleroma.foo.bar/users/2345", true),
- arrayOf("https://pleroma.foo.bar/notice/9", true),
- arrayOf("https://pleroma.foo.bar/notice/9345678", true),
- arrayOf("https://pleroma.foo.bar/notice/wat", true),
- arrayOf("https://pleroma.foo.bar/notice/9qTHT2ANWUdXzENqC0", true),
- arrayOf("https://pleroma.foo.bar/objects/abcdef-123-abcd-9876543", true),
- arrayOf("https://misskey.foo.bar/notes/mew", true),
- arrayOf("https://misskey.foo.bar/notes/1421564653", true),
- arrayOf("https://misskey.foo.bar/notes/qwer615985ddf", true),
- arrayOf("https://friendica.foo.bar/profile/user", true),
- arrayOf("https://friendica.foo.bar/profile/uSeR", true),
- arrayOf("https://friendica.foo.bar/profile/user_user", true),
- arrayOf("https://friendica.foo.bar/profile/123", true),
- arrayOf("https://friendica.foo.bar/display/abcdef-123-abcd-9876543", true),
- arrayOf("https://google.com/", false),
- arrayOf("https://mastodon.foo.bar/@User?foo=bar", false),
- arrayOf("https://mastodon.foo.bar/@User#foo", false),
- arrayOf("http://mastodon.foo.bar/@", false),
- arrayOf("http://mastodon.foo.bar/@/345678", false),
- arrayOf("https://mastodon.foo.bar/@user/345667890345678/", false),
- arrayOf("https://mastodon.foo.bar/@user/3abce", false),
- arrayOf("https://pleroma.foo.bar/users/", false),
- arrayOf("https://pleroma.foo.bar/users/meow/", false),
- arrayOf("https://pleroma.foo.bar/users/@meow", false),
- arrayOf("https://pleroma.foo.bar/user/2345", false),
- arrayOf("https://pleroma.foo.bar/notices/123456", false),
- arrayOf("https://pleroma.foo.bar/notice/@neverhappen/", false),
- arrayOf("https://pleroma.foo.bar/object/abcdef-123-abcd-9876543", false),
- arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543", false),
- arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543/", false),
- arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd_9876543", false),
- arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd-9876543", false),
- arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd-9876543/", false),
- arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd_9876543", false),
- arrayOf("https://friendica.foo.bar/profile/@mew", false),
- arrayOf("https://friendica.foo.bar/profile/@mew/", false),
- arrayOf("https://misskey.foo.bar/notes/@nyan", false),
- arrayOf("https://misskey.foo.bar/notes/NYAN123", false),
- arrayOf("https://misskey.foo.bar/notes/meow123/", false),
- arrayOf("https://pixelfed.social/p/connyduck/391263492998670833", true),
- arrayOf("https://pixelfed.social/connyduck", true)
- )
- }
- }
-
- @Test
- fun test() {
- assertEquals(expectedResult, looksLikeMastodonUrl(url))
- }
- }
-
@Test
fun beginEndSearch_setIsSearching_isSearchingAfterBegin() {
activity.onBeginSearch("https://mastodon.foo.bar/@User")
diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt
index cc946194a..c133e75d9 100644
--- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt
@@ -458,6 +458,23 @@ class ComposeActivityTest {
assertEquals(language, activity.selectedLanguage)
}
+ @Test
+ fun modernLanguageCodeIsUsed() {
+ // https://github.com/tuskyapp/Tusky/issues/2903
+ // "ji" was deprecated in favor of "yi"
+ composeOptions = ComposeActivity.ComposeOptions(language = "ji")
+ setupActivity()
+ assertEquals("yi", activity.selectedLanguage)
+ }
+
+ @Test
+ fun unknownLanguageGivenInComposeOptionsIsRespected() {
+ val language = "zzz"
+ composeOptions = ComposeActivity.ComposeOptions(language = language)
+ setupActivity()
+ assertEquals(language, activity.selectedLanguage)
+ }
+
private fun clickUp() {
val menuItem = RoboMenuItem(android.R.id.home)
activity.onOptionsItemSelected(menuItem)
diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt
index 449f0cf52..51258f0e6 100644
--- a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt
@@ -258,6 +258,7 @@ class FilterTest {
reblog = null,
content = content,
createdAt = Date(),
+ editedAt = null,
emojis = emptyList(),
reblogsCount = 0,
favouritesCount = 0,
diff --git a/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt
index 93388a328..05da15075 100644
--- a/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt
@@ -108,7 +108,8 @@ class MainActivityTest {
url = "https://mastodon.example/@ConnyDuck",
avatar = "https://mastodon.example/system/accounts/avatars/000/150/486/original/ab27d7ddd18a10ea.jpg"
),
- status = null
+ status = null,
+ report = null,
),
accountEntity,
true
diff --git a/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt
index 9598f2c1e..a9b066319 100644
--- a/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt
@@ -16,9 +16,6 @@
package com.keylesspalace.tusky
import android.app.Application
-import android.content.Context
-import android.content.res.Configuration
-import com.keylesspalace.tusky.util.LocaleManager
import de.c1710.filemojicompat_defaults.DefaultEmojiPackList
import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper
@@ -29,19 +26,4 @@ class TuskyApplication : Application() {
super.onCreate()
EmojiPackHelper.init(this, DefaultEmojiPackList.get(this))
}
-
- override fun attachBaseContext(base: Context) {
- localeManager = LocaleManager(base)
- super.attachBaseContext(localeManager.setLocale(base))
- }
-
- override fun onConfigurationChanged(newConfig: Configuration) {
- super.onConfigurationChanged(newConfig)
- localeManager.setLocale(this)
- }
-
- companion object {
- @JvmStatic
- lateinit var localeManager: LocaleManager
- }
}
diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt
index a4791e6fa..7b48a3bc9 100644
--- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt
@@ -34,6 +34,7 @@ fun mockStatus(
reblog = null,
content = "Test",
createdAt = fixedDate,
+ editedAt = null,
emojis = emptyList(),
reblogsCount = 1,
favouritesCount = 2,
diff --git a/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt b/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt
index 7050b3271..516d9c64c 100644
--- a/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt
@@ -440,6 +440,7 @@ class TimelineDaoTest {
inReplyToAccountId = "inReplyToAccountId$statusId",
content = "Content!$statusId",
createdAt = createdAt,
+ editedAt = null,
emojis = "emojis$statusId",
reblogsCount = 1 * statusId.toInt(),
favouritesCount = 2 * statusId.toInt(),
diff --git a/app/src/test/java/com/keylesspalace/tusky/usecase/TimelineCasesTest.kt b/app/src/test/java/com/keylesspalace/tusky/usecase/TimelineCasesTest.kt
index f6e9ccf43..d6b3eabf6 100644
--- a/app/src/test/java/com/keylesspalace/tusky/usecase/TimelineCasesTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/usecase/TimelineCasesTest.kt
@@ -77,6 +77,7 @@ class TimelineCasesTest {
reblog = null,
content = "",
createdAt = Date(),
+ editedAt = null,
emojis = emptyList(),
reblogsCount = 0,
favouritesCount = 0,
diff --git a/app/src/test/java/com/keylesspalace/tusky/util/LinkHelperTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/LinkHelperTest.kt
index 51e4ec800..e6bc2eb01 100644
--- a/app/src/test/java/com/keylesspalace/tusky/util/LinkHelperTest.kt
+++ b/app/src/test/java/com/keylesspalace/tusky/util/LinkHelperTest.kt
@@ -12,6 +12,7 @@ import com.keylesspalace.tusky.interfaces.LinkListener
import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
import org.robolectric.annotation.Config
@Config(sdk = [28])
@@ -205,11 +206,14 @@ class LinkHelperTest {
fun nonUriTextExactlyMatchingDomainIsNotMarkedUp() {
val domain = "some.place"
val content = SpannableStringBuilder()
- .append(domain, URLSpan("https://some.place/"), 0)
- .append(domain, URLSpan("https://some.place"), 0)
- .append(domain, URLSpan("https://www.some.place"), 0)
- .append("www.$domain", URLSpan("https://some.place"), 0)
- .append("www.$domain", URLSpan("https://some.place/"), 0)
+ .append(domain, URLSpan("https://$domain/"), 0)
+ .append(domain, URLSpan("https://$domain"), 0)
+ .append(domain, URLSpan("https://www.$domain"), 0)
+ .append("www.$domain", URLSpan("https://$domain"), 0)
+ .append("www.$domain", URLSpan("https://$domain/"), 0)
+ .append("$domain/", URLSpan("https://$domain/"), 0)
+ .append("$domain/", URLSpan("https://$domain"), 0)
+ .append("$domain/", URLSpan("https://www.$domain"), 0)
val markedUpContent = markupHiddenUrls(context, content)
Assert.assertFalse(markedUpContent.contains("🔗"))
@@ -305,4 +309,71 @@ class LinkHelperTest {
Assert.assertFalse(markedUpContent.contains("${getDomain(tag.url)})"))
}
}
+
+ @RunWith(Parameterized::class)
+ class UrlMatchingTests(private val url: String, private val expectedResult: Boolean) {
+ companion object {
+ @Parameterized.Parameters(name = "match_{0}")
+ @JvmStatic
+ fun data(): Iterable {
+ return listOf(
+ arrayOf("https://mastodon.foo.bar/@User", true),
+ arrayOf("http://mastodon.foo.bar/@abc123", true),
+ arrayOf("https://mastodon.foo.bar/@user/345667890345678", true),
+ arrayOf("https://mastodon.foo.bar/@user/3", true),
+ arrayOf("https://pleroma.foo.bar/users/meh3223", true),
+ arrayOf("https://pleroma.foo.bar/users/meh3223_bruh", true),
+ arrayOf("https://pleroma.foo.bar/users/2345", true),
+ arrayOf("https://pleroma.foo.bar/notice/9", true),
+ arrayOf("https://pleroma.foo.bar/notice/9345678", true),
+ arrayOf("https://pleroma.foo.bar/notice/wat", true),
+ arrayOf("https://pleroma.foo.bar/notice/9qTHT2ANWUdXzENqC0", true),
+ arrayOf("https://pleroma.foo.bar/objects/abcdef-123-abcd-9876543", true),
+ arrayOf("https://misskey.foo.bar/notes/mew", true),
+ arrayOf("https://misskey.foo.bar/notes/1421564653", true),
+ arrayOf("https://misskey.foo.bar/notes/qwer615985ddf", true),
+ arrayOf("https://friendica.foo.bar/profile/user", true),
+ arrayOf("https://friendica.foo.bar/profile/uSeR", true),
+ arrayOf("https://friendica.foo.bar/profile/user_user", true),
+ arrayOf("https://friendica.foo.bar/profile/123", true),
+ arrayOf("https://friendica.foo.bar/display/abcdef-123-abcd-9876543", true),
+ arrayOf("https://google.com/", false),
+ arrayOf("https://mastodon.foo.bar/@User?foo=bar", false),
+ arrayOf("https://mastodon.foo.bar/@User#foo", false),
+ arrayOf("http://mastodon.foo.bar/@", false),
+ arrayOf("http://mastodon.foo.bar/@/345678", false),
+ arrayOf("https://mastodon.foo.bar/@user/345667890345678/", false),
+ arrayOf("https://mastodon.foo.bar/@user/3abce", false),
+ arrayOf("https://pleroma.foo.bar/users/", false),
+ arrayOf("https://pleroma.foo.bar/users/meow/", false),
+ arrayOf("https://pleroma.foo.bar/users/@meow", false),
+ arrayOf("https://pleroma.foo.bar/user/2345", false),
+ arrayOf("https://pleroma.foo.bar/notices/123456", false),
+ arrayOf("https://pleroma.foo.bar/notice/@neverhappen/", false),
+ arrayOf("https://pleroma.foo.bar/object/abcdef-123-abcd-9876543", false),
+ arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543", false),
+ arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543/", false),
+ arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd_9876543", false),
+ arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd-9876543", false),
+ arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd-9876543/", false),
+ arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd_9876543", false),
+ arrayOf("https://friendica.foo.bar/profile/@mew", false),
+ arrayOf("https://friendica.foo.bar/profile/@mew/", false),
+ arrayOf("https://misskey.foo.bar/notes/@nyan", false),
+ arrayOf("https://misskey.foo.bar/notes/NYAN123", false),
+ arrayOf("https://misskey.foo.bar/notes/meow123/", false),
+ arrayOf("https://pixelfed.social/p/connyduck/391263492998670833", true),
+ arrayOf("https://pixelfed.social/connyduck", true),
+ arrayOf("https://gts.foo.bar/@goblin/statuses/01GH9XANCJ0TA8Y95VE9H3Y0Q2", true),
+ arrayOf("https://gts.foo.bar/@goblin", true),
+ arrayOf("https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5", true),
+ )
+ }
+ }
+
+ @Test
+ fun test() {
+ Assert.assertEquals(expectedResult, looksLikeMastodonUrl(url))
+ }
+ }
}
diff --git a/app/src/test/java/com/keylesspalace/tusky/util/TimestampUtilsTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/TimestampUtilsTest.kt
new file mode 100644
index 000000000..ded3b219f
--- /dev/null
+++ b/app/src/test/java/com/keylesspalace/tusky/util/TimestampUtilsTest.kt
@@ -0,0 +1,29 @@
+package com.keylesspalace.tusky.util
+
+import android.content.Context
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.keylesspalace.tusky.R
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.robolectric.annotation.Config
+
+private const val STATUS_CREATED_AT_NOW = "test"
+
+@Config(sdk = [28])
+@RunWith(AndroidJUnit4::class)
+class TimestampUtilsTest {
+ private val ctx: Context = mock {
+ on { getString(R.string.status_created_at_now) } doReturn STATUS_CREATED_AT_NOW
+ }
+
+ @Test
+ fun shouldShowNowForSmallTimeSpans() {
+ assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 0, 300))
+ assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 300, 0))
+ assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 501, 0))
+ assertEquals(STATUS_CREATED_AT_NOW, TimestampUtils.getRelativeTimeSpanString(ctx, 0, 999))
+ }
+}
diff --git a/fastlane/metadata/android/cs/changelogs/58.txt b/fastlane/metadata/android/cs/changelogs/58.txt
new file mode 100644
index 000000000..0822c58df
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/58.txt
@@ -0,0 +1,13 @@
+Tusky v6.0
+
+- Filtry časové osy jsme přesunuli do Předvoleb účtu a budou se synchronizovat se serverem.
+- Na hlavním stránce můžete mít vlastní hashtag jako kartu
+- Seznamy lze upravovat
+- Zabezpečení: odstraněna podpora TLS 1.0 a TLS 1.1 a přidána podpora TLS 1.3 v systému Android 6+.
+- Vlastní emotikony jsou nabízeny při psaní
+- Nové nastavení motivu "následovat systémový motiv"
+- Vylepšená přístupnost časové osy
+- Tusky teď ignoruje neznámá oznámení a už nepadá
+- Nové nastavení: Nyní můžete v aplikaci Tusky nastavit jiný než systímový jazyk.
+- Nové překlady: čeština (!) a esperanto
+- Mnoho dalších vylepšení a oprav
diff --git a/fastlane/metadata/android/cs/changelogs/70.txt b/fastlane/metadata/android/cs/changelogs/70.txt
new file mode 100644
index 000000000..ac9aef107
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/70.txt
@@ -0,0 +1,8 @@
+Tusky v10.0
+
+- záložky na statusy a seznam záložek.
+- plánování tootů: Čas, který vyberete, musí být alespoň 5 minut v budoucnosti.
+- seznamy na hlavní obrazovce.
+- odesílání zvukových příloh.
+
+A spousta dalších drobných vylepšení a oprav chyb!
diff --git a/fastlane/metadata/android/cs/changelogs/72.txt b/fastlane/metadata/android/cs/changelogs/72.txt
new file mode 100644
index 000000000..22da5ff96
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/72.txt
@@ -0,0 +1,11 @@
+Tusky v11.0
+
+- ipozornění na nové žádosti o sledování, když je váš účet uzamčen
+- nové funkce, které lze zapínat v předvolebách:
+ - swajpování mezi kartami
+ - potvrzení boostu
+ - zobrazení náhledů odkazů v časových osách
+- konverzace lze ztlumit
+- Výsledky ankety se nyní budou počítat na základě počtu hlasujících, a ne na základě celkového počtu hlasů, což usnadňuje přehlednost anket s více možnostmi volby.
+- oprava mnoha chyb, z nichž většina se týká psaní tootů
+- vylepšené překlady
diff --git a/fastlane/metadata/android/cs/changelogs/74.txt b/fastlane/metadata/android/cs/changelogs/74.txt
new file mode 100644
index 000000000..c62a7e47d
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/74.txt
@@ -0,0 +1,8 @@
+Tusky v12.0
+
+- Vylepšená hlavní obrazovka - karty lze nyní přesunout do spodní části.
+- Při ztlumení uživatele se nyní můžete také rozhodnout, zda chcete ztlumit jeho oznámení
+- Nyní můžete sledovat libovolný počet hashtagů na jedné kartě hashtagů
+- Vylepšený způsob zobrazování popisů médií, takže funguje i u superdlouhých popisů
+
+Úplný seznam změn: https://github.com/tuskyapp/Tusky/releases
diff --git a/fastlane/metadata/android/cs/changelogs/77.txt b/fastlane/metadata/android/cs/changelogs/77.txt
new file mode 100644
index 000000000..54b71b9fe
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/77.txt
@@ -0,0 +1,10 @@
+Tusky v13.0
+
+- podpora poznámek k profilům (Mastodon 3.2.0)
+- podpora oznámení pro správce (Mastodon 3.1.0)
+
+- avatar vybraného účtu se nyní zobrazuje na hlavním panelu nástrojů
+- kliknutím na zobrazené jméno na časové ose se nyní otevře profilová stránka daného uživatele
+
+- mnoho oprav chyb a drobných vylepšení
+- vylepšené překlady
diff --git a/fastlane/metadata/android/cs/changelogs/80.txt b/fastlane/metadata/android/cs/changelogs/80.txt
new file mode 100644
index 000000000..d7c8c10a9
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/80.txt
@@ -0,0 +1,8 @@
+Tusky v14.0
+
+- upozornění na příspěvky sledovaného uživatele - klikněte na ikonu zvonku na jeho profilu! (funkce Mastodon 3.3.0)
+- redesign funkce návrhu. Teď je rychlejší, uživatelsky přívětivější a méně chybová.
+- Byl přidán nový zen režim, který umožňuje omezit některé funkce Tusky.
+- Tusky nyní umí animovat vlastní emotikony.
+
+Úplný seznam změn: https://github.com/tuskyapp/Tusky/releases
diff --git a/fastlane/metadata/android/cs/changelogs/82.txt b/fastlane/metadata/android/cs/changelogs/82.txt
new file mode 100644
index 000000000..29dcb1c62
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/82.txt
@@ -0,0 +1,5 @@
+Tusky v15.0
+
+- Žádosti o sledování se vždy zobrazují v hlavní nabídce.
+- Výběr času pro naplánování příspěvku má teď design odpovídající zbytku aplikace.
+Úplný seznam změn: https://github.com/tuskyapp/Tusky/releases
diff --git a/fastlane/metadata/android/cs/changelogs/83.txt b/fastlane/metadata/android/cs/changelogs/83.txt
new file mode 100644
index 000000000..ae0ac5240
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/83.txt
@@ -0,0 +1,3 @@
+Tusky v15.1
+
+Toto vydání opravuje pád při popisování obrázků
diff --git a/fastlane/metadata/android/cs/changelogs/87.txt b/fastlane/metadata/android/cs/changelogs/87.txt
new file mode 100644
index 000000000..746d196ba
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/87.txt
@@ -0,0 +1,8 @@
+Tusky v16.0
+
+- Logika načítání časové osy byla kompletně přepsána, aby byla rychlejší, méně chybová a jednodušší na údržbu.
+- Tusky nyní umí animovat vlastní emotikony ve formátu APNG a Animated WebP.
+- mnoho opravených chyb
+- podpora systému Android 11
+- nové překlady: skotská gaelština, galicijština, ukrajinština.
+- vylepšené překlady
diff --git a/fastlane/metadata/android/cs/changelogs/89.txt b/fastlane/metadata/android/cs/changelogs/89.txt
new file mode 100644
index 000000000..f06d8404e
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/89.txt
@@ -0,0 +1,7 @@
+Tusky v17.0
+
+- "Otevřít jako..." je nyní k dispozici také v nabídce profilů účtů při použití více účtů.
+- Přihlášení je nyní zpracováváno ve webovém zobrazení v rámci aplikace
+- podpora systému Android 12
+- podpora nového API pro konfiguraci instancí Mastodon
+- a mnoho dalších drobných oprav a vylepšení
diff --git a/fastlane/metadata/android/cs/changelogs/91.txt b/fastlane/metadata/android/cs/changelogs/91.txt
new file mode 100644
index 000000000..ab56d3d8c
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/91.txt
@@ -0,0 +1,6 @@
+Tusky v18.0
+
+- podpora nových typů oznámení (Mastodon 3.5)
+- Odznak bota nyní vypadá lépe a přizpůsobuje se zvolenému tématu.
+- V zobrazení detailu příspěvku lze nyní vybrat text
+- Opraveno mnoho chyb, včetně jedné, která znemožňovala přihlášení v systému Android 6 a nižším.
diff --git a/fastlane/metadata/android/cs/changelogs/94.txt b/fastlane/metadata/android/cs/changelogs/94.txt
new file mode 100644
index 000000000..4808320b8
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/94.txt
@@ -0,0 +1,9 @@
+Tusky 19.0
+
+- podpora Unified Push: Pro aktivaci podpory se musíte znovu přihlásit ke svým účtům.
+- Počet odpovědí na příspěvek se nyní zobrazuje v časových osách.
+- Obrázky lze nyní při vytváření příspěvku oříznout.
+- U profilů se nyní zobrazuje datum jejich vytvoření.
+- Při prohlížení seznamu se nyní na panelu nástrojů zobrazuje jeho název.
+- Mnoho opravených chyb
+- Vylepšení překladů
diff --git a/fastlane/metadata/android/cs/changelogs/97.txt b/fastlane/metadata/android/cs/changelogs/97.txt
new file mode 100644
index 000000000..c7325957c
--- /dev/null
+++ b/fastlane/metadata/android/cs/changelogs/97.txt
@@ -0,0 +1,9 @@
+Tusky 20.0
+
+- Nová ikona aplikace od Dzuk https://dzuk.zone/
+- Nyní můžete sledovat hashtagy. Klikněte na hashtag a poté na ikonu v panelu nástrojů.
+- podpora systému Android 13
+- Nová rozbalovací nabídka v při psaní příspěvku, která umožňuje nastavit jazyk příspěvku.
+- Karta médií v profilech nyní respektuje citlivá média a načítá se plynuleji.
+- Před odesláním příspěvku je nyní možné nastavit bod střed zvětšení obrázku.
+- nová možnost zobrazení celého uživatelského jména v panelu nástrojů
diff --git a/fastlane/metadata/android/de/changelogs/91.txt b/fastlane/metadata/android/de/changelogs/91.txt
new file mode 100644
index 000000000..1fd95cb23
--- /dev/null
+++ b/fastlane/metadata/android/de/changelogs/91.txt
@@ -0,0 +1,6 @@
+Tusky v18.0
+
+- Unterstützung für neue Benachrichtigungstypen aus Mastodon 3.5
+- Das Bot-Symbol sieht jetzt besser aus und passt sich dem gewählten App-Thema an
+- Der Text in den Beitragsdetails kann jetzt ausgewählt werden
+- Viele Fehler behoben, inklusive einem, der Anmeldungen auf Android 6 und älter verhindert hat
diff --git a/fastlane/metadata/android/de/changelogs/97.txt b/fastlane/metadata/android/de/changelogs/97.txt
new file mode 100644
index 000000000..90a94829e
--- /dev/null
+++ b/fastlane/metadata/android/de/changelogs/97.txt
@@ -0,0 +1,9 @@
+Tusky 20.0
+
+- Neues App-Icon von Dzuk https://dzuk.zone
+- Du kannst nun Hashtags folgen. Tippe einen Hashtag und anschließend das Symbol in der Hauptleiste.
+- Unterstützung für Android 13
+- Neues Auswahlmenü zum festlegen der Beitragssprache
+- Der Medien-Tab in Profilen achtet nun Medien mit Inhaltswarnung und lädt schneller
+- Es ist nun möglich den Fokuspunkt eines Bildes vor der Veröffentlichung festzulegen
+- Du kannst nun deinen vollständigen Nutzernamen in der Hauptleiste anzeigen lassen
diff --git a/fastlane/metadata/android/en-US/changelogs/97.txt b/fastlane/metadata/android/en-US/changelogs/97.txt
new file mode 100644
index 000000000..146cf2479
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/97.txt
@@ -0,0 +1,9 @@
+Tusky 20.0
+
+- New App icon by Dzuk https://dzuk.zone/
+- You can now follow hashtags. Click on a hashtag and then on the icon in the toolbar.
+- Support for Android 13
+- new dropdown in the compose view to set the language of a post
+- The media tab in profiles now respects sensitive media and loads smoother.
+- It is now possible to set the focus point of an image before posting
+- New option to show your full username in the toolbar
\ No newline at end of file
diff --git a/fastlane/metadata/android/es/changelogs/87.txt b/fastlane/metadata/android/es/changelogs/87.txt
new file mode 100644
index 000000000..4dd749429
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/87.txt
@@ -0,0 +1,8 @@
+Tusky v6.0
+
+- La lógica de carga de linea de tiempo fue reescrita para ser más rápida, tener menos bugs, y ser más fácil de mantener.
+- Tusky puede animar emojis personalizados en formatos APNG y Animated WebP
+- Muchos arreglos de bugs
+- Soporte para Android 11
+- Nuevas traducciones: Gaélico escocés, Galiciano y Ucraniano
+- Traducciones mejoradas
diff --git a/fastlane/metadata/android/es/changelogs/89.txt b/fastlane/metadata/android/es/changelogs/89.txt
new file mode 100644
index 000000000..6d004d92e
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/89.txt
@@ -0,0 +1,7 @@
+Tusky versión 17.0
+
+-"Abrir como..." ahora disponible en el menú del perfil cuando se usan varias cuentas.
+- Para el registro ahora se utiliza WebView dentro de la app
+- Soporte para Android 12
+- Soporte para la nueva API de configuración de instancias de Mastodon
+- y muchas otras mejoras y correcciones
diff --git a/fastlane/metadata/android/es/changelogs/91.txt b/fastlane/metadata/android/es/changelogs/91.txt
new file mode 100644
index 000000000..2478c766d
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/91.txt
@@ -0,0 +1,6 @@
+Tusky versión 18.0
+
+- Soporte para las nuevas notificaciones de Mastodon 3.5
+- El símbolo de bot ahora se adecúa mejor al tema seleccionado
+- Ahora se puede seleccionar el texto de una publicación en la vista detallada
+- Correcciones de muchos errores, incluyendo el que impedía registros en Android 6 y anteriores
diff --git a/fastlane/metadata/android/es/changelogs/94.txt b/fastlane/metadata/android/es/changelogs/94.txt
new file mode 100644
index 000000000..8a59f9df1
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/94.txt
@@ -0,0 +1,9 @@
+Tusky versión 19.0
+
+- Soporte para Unified Push. Para activar el soporte tendrás que volver a iniciar sesión en tus cuentas.
+- Ahora se muestra el número de respuestas a una publicación en las cronologías.
+- Ahora se pueden recortar imágenes cuando se escribe una publicación.
+- Ahora se muestra la fecha en la que se crearon los perfiles.
+- Cuando se ve una lista, ahora se muestra el nombre en la barra de herramientas.
+- Correcciones de diversos errores
+- Mejoras en las traducciones
diff --git a/fastlane/metadata/android/es/changelogs/97.txt b/fastlane/metadata/android/es/changelogs/97.txt
new file mode 100644
index 000000000..09c985116
--- /dev/null
+++ b/fastlane/metadata/android/es/changelogs/97.txt
@@ -0,0 +1,9 @@
+Tusky versión 20.0
+
+- Nuevo icono de la aplicación, de Dzuk https://dzuk.zone/
+- Ahora se pueden seguir etiquetas. Clica en una etiqueta y después en el icono de la barra.
+- Soporte para Android 13
+- Se puede seleccionar el idioma en el que se escribe una publicación
+- La pestaña de multimedia en los perfiles ahora respeta el contenido sensible y carga mejor.
+- Ahora es posible fijar el foco de una imagen antes de publicarla
+- Nueva opción para mostrar el nombre de usuario completo en la barra
diff --git a/fastlane/metadata/android/fa/changelogs/97.txt b/fastlane/metadata/android/fa/changelogs/97.txt
new file mode 100644
index 000000000..7b810a1f6
--- /dev/null
+++ b/fastlane/metadata/android/fa/changelogs/97.txt
@@ -0,0 +1,9 @@
+تاسکی ۲۰٫۰
+
+- نقشککارهٔ جدید به دست https://dzuk.zone
+- اکنون میتوانید برچسبها را دنبال کنید. روی برچسبی ز ده و سپس نقشک داخل نوارابزار را بزنید.
+- پشتیبانی از اندروید ۱۳
+- پایینافتادنی جدید در نمای نوشتن برای تنظیم زبان فرسته
+- زبانهٔ رسانه در نمایه اکنون به رسانههای خسّاس احترام گذاشته و نرمتر بار میشود.
+- اکنون میتوان پیش از فرستادن تصویر، نقطهٔ تمرکز را تنظیم کرد
+- گزینهٔ جدید برای نمایش نام کاربری کاملتان در نوارابزار
diff --git a/fastlane/metadata/android/fr/changelogs/97.txt b/fastlane/metadata/android/fr/changelogs/97.txt
new file mode 100644
index 000000000..07d9b8e06
--- /dev/null
+++ b/fastlane/metadata/android/fr/changelogs/97.txt
@@ -0,0 +1,9 @@
+Tusky 20.0
+
+- Nouvelle icône d'application par https://dzuk.zone/
+- Vous pouvez maintenant suivre des mot croisillon. Cliquer sur un mot croisillon puis sur l’icône dans la barre d'outil.
+- Support de Android 13
+- Nouveau menu déroulant dans la fenêtre de composition permettant de choisir la langue de la publication.
+- L'onglet Media dans les profiles respecte maintenant les images sensibles et charge de façon plus fluide.
+- Ajout de la possibilité de choisir la partie visible d'une image avant de publier.
+- Nouvelle option pour voir votre nom d'utilisateur complet dans la barre d'outils.
diff --git a/fastlane/metadata/android/gl/changelogs/97.txt b/fastlane/metadata/android/gl/changelogs/97.txt
new file mode 100644
index 000000000..5c707f7e2
--- /dev/null
+++ b/fastlane/metadata/android/gl/changelogs/97.txt
@@ -0,0 +1,9 @@
+Tusky 20.0
+
+- Nova icona da app por Dzuk https://dzuk.zone/
+- Podes seguir cancelos. Preme no cancelo e depois na icona da barra de ferramentas.
+- Soporte para Android 13
+- Cando escribes unha mensaxe podes seleccionar o idioma da publicación
+- Nos perfís, a lapela multimedia agora respecta o marcado como sensible e carga máis suavemente.
+- É posible establecer o foco nunha zona da imaxe antes de publicala
+- Nova opcións para mostrar o identificador de usuaria completo na barra de ferramentas
diff --git a/fastlane/metadata/android/nb-NO/changelogs/97.txt b/fastlane/metadata/android/nb-NO/changelogs/97.txt
new file mode 100644
index 000000000..a379f9403
--- /dev/null
+++ b/fastlane/metadata/android/nb-NO/changelogs/97.txt
@@ -0,0 +1,9 @@
+Tusky 20.0
+
+- Nytt applikasjonsikon av Dzuk https://dzuk.zone/
+- Du kan nå følge stikkord. Klikk på et stikkord, og deretter ikonet i verktøylinjen.
+- Støtte for Android 13
+- Ny nedtrekksliste for å konfigurere hvilket språk et innlegg er skrevet på
+- Media-fanen i profilvisning håndterer nå sensitivt media og laster fortere
+- Det er nå mulig å sette fokuspunkt på et bilde før det publiseres
+- Mulighet for å vise ditt fulle navn på verktøylinjen
diff --git a/fastlane/metadata/android/sv/changelogs/97.txt b/fastlane/metadata/android/sv/changelogs/97.txt
new file mode 100644
index 000000000..bf18d815d
--- /dev/null
+++ b/fastlane/metadata/android/sv/changelogs/97.txt
@@ -0,0 +1,9 @@
+Tusky 20.0
+
+- Ny appikon av Dzuk https://dzuk.zone/
+- Du kan nu följa hashtaggar. Tryck på en hashtagg och sen på ikonen i verktygsraden
+- Stöd för Android 13
+- Ny rullgardinsmeny för att ställa in vilket språk inlägget är skrivet på
+- Mediafliken i profilvyn hanterar nu känsligt media och laddar snabbare
+- Det går nu att sätta fokuspunkt för en bild innan den publiceras
+- Nytt alternativ för att visa ditt fullständiga namn i verktygsraden
diff --git a/fastlane/metadata/android/uk/changelogs/97.txt b/fastlane/metadata/android/uk/changelogs/97.txt
new file mode 100644
index 000000000..233daff73
--- /dev/null
+++ b/fastlane/metadata/android/uk/changelogs/97.txt
@@ -0,0 +1,9 @@
+Tusky 20.0
+
+- Нова піктограма застосунку від Dzuk https://dzuk.zone/
+- Додано можливість слідкувати за хештегами. Натисніть на нього, а потім на піктограму на панелі інструментів.
+- Підтримка Android 13
+- новий спадний список у вікні створення допису для вибору мови допису
+- Вкладка «Медіа» у профілях завантажується плавніше для чутливих носіїв.
+- З'явилася можливість установити фокусувати зображення перед оприлюдненням
+- Нова опція показу вашого повного імені користувача на панелі інструментів
diff --git a/fastlane/metadata/android/vi/changelogs/97.txt b/fastlane/metadata/android/vi/changelogs/97.txt
new file mode 100644
index 000000000..492c72172
--- /dev/null
+++ b/fastlane/metadata/android/vi/changelogs/97.txt
@@ -0,0 +1,9 @@
+Tusky 20.0
+
+- Biểu tượng app mới của Dzuk https://dzuk.zone/
+- Theo dõi hashtag. Nhấn vào một hashtag và sau đó nhấn vào biểu tượng trên thanh công cụ.
+- Hỗ trợ Android 13
+- Chọn ngôn ngữ của tút
+- Tab media trong hồ sơ hiện tôn trọng media nhạy cảm và tải mượt mà hơn.
+- Đặt điểm lấy nét của ảnh trước khi đăng
+- Tùy chọn mới để hiển thị tên người dùng đầy đủ của bạn trên thanh công cụ
diff --git a/fastlane/metadata/android/zh-Hans/changelogs/89.txt b/fastlane/metadata/android/zh-Hans/changelogs/89.txt
new file mode 100644
index 000000000..808af8e6e
--- /dev/null
+++ b/fastlane/metadata/android/zh-Hans/changelogs/89.txt
@@ -0,0 +1,7 @@
+Tusky v17.0
+
+- 使用多个帐户时,“打开为...”现在也可以在帐户配置文件的菜单中使用。
+- 登录现在在内嵌的 WebView 中处理。
+- 支持 Android 12。
+- 支持新的 Mastodon 实例配置 API。
+- 一些其它小修复和改动
diff --git a/fastlane/metadata/android/zh-Hans/changelogs/91.txt b/fastlane/metadata/android/zh-Hans/changelogs/91.txt
new file mode 100644
index 000000000..e91fe6db3
--- /dev/null
+++ b/fastlane/metadata/android/zh-Hans/changelogs/91.txt
@@ -0,0 +1,6 @@
+Tusky v18.0
+
+- 支持新的 Mastodon 3.5 通知类型。
+- 机器人徽章现在看起来更棒,并将适应所选主题。
+- 嘟文详情视图上的文本现在可以被选择。
+- 修复了一些 bugs,其中有一个会阻止在 Android 6 及更低版本的设备上登录
diff --git a/fastlane/metadata/android/zh-Hans/changelogs/94.txt b/fastlane/metadata/android/zh-Hans/changelogs/94.txt
new file mode 100644
index 000000000..f63300638
--- /dev/null
+++ b/fastlane/metadata/android/zh-Hans/changelogs/94.txt
@@ -0,0 +1,9 @@
+Tusky 19.0
+
+- 支持统一推送。 要激活支持,您必须重新登录您的帐户。
+- 嘟文的回复数量现在显示在时间轴中。
+- 现在可以在撰写嘟文时裁剪图片。
+- 配置文件现在将显示创建日期。
+- 查看列表时,标题将显示在工具栏中。
+- 很多错误修正。
+- 翻译改进
diff --git a/fastlane/metadata/android/zh-Hans/changelogs/97.txt b/fastlane/metadata/android/zh-Hans/changelogs/97.txt
new file mode 100644
index 000000000..4879192bd
--- /dev/null
+++ b/fastlane/metadata/android/zh-Hans/changelogs/97.txt
@@ -0,0 +1,9 @@
+Tusky 20.0
+
+- 来自 Dzuk 的新图标 https://dzuk.zone/
+- 您现在可以关注主题标签。 点击主题标签后再点击工具栏中的图标即可。
+- 支持 Android 13
+- 撰写视图中用于设置嘟文语言的新下拉菜单
+- 配置文件中的媒体选项卡现在尊重敏感媒体且加载得更流畅。
+- 现在可以在发布前设置图片的焦点
+- 在工具栏中显示完整用户名的新选项
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 4fd919310..8b27dc3e7 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,7 +2,7 @@
accelf-easter = "1.0.2"
agp = "7.2.2"
androidx-activity = "1.6.0"
-androidx-appcompat = "1.5.1"
+androidx-appcompat = "1.6.0-rc01"
androidx-browser = "1.4.0"
androidx-cardview = "1.0.0"
androidx-constraintlayout = "2.1.4"