From 378e87033ea90122886399cd86d4e4da881acb71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Fri, 15 Jul 2022 14:26:58 +0000 Subject: [PATCH 001/142] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (489 of 489 strings) Co-authored-by: Hồ Nhất Duy Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ Translation: Tusky/Tusky --- app/src/main/res/values-vi/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 7eb9ba336..5b117fa8a 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -236,7 +236,7 @@ Báo động Thông báo Thông báo - Nhắn riêng: Chỉ người được nhắc đến thấy + Nhắn riêng: Chỉ người được nhắc đến Riêng tư: Chỉ người theo dõi Hạn chế: Không hiện trên bảng tin Công khai: Mọi người đều thấy From 25f637f0a8d6609b6078cf9a2089b3546f3e5dd6 Mon Sep 17 00:00:00 2001 From: Constantin A <10349490+C1710@users.noreply.github.com> Date: Mon, 25 Jul 2022 12:36:35 +0200 Subject: [PATCH 002/142] Set FilemojiCompat to version 3.2.3 (#2611) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 70649f429..981ef3ef8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -101,7 +101,7 @@ ext.glideVersion = '4.13.1' ext.daggerVersion = '2.42' ext.materialdrawerVersion = '8.4.5' ext.emoji2_version = '1.1.0' -ext.filemojicompat_version = '3.2.2' +ext.filemojicompat_version = '3.2.3' // if libraries are changed here, they should also be changed in LicenseActivity dependencies { From 1b6a0908f63be393f47fc65505e90ac7cf239462 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 26 Jul 2022 20:24:50 +0200 Subject: [PATCH 003/142] Handle even more instance defaults (#2612) * handle media size instance limits * remove unused attributes from Instance entity * support max_media_attachments * support pleroma field limits, remove max_bio_chars support * improve field input margin * fix tests * MAX_ACCOUNT_FIELDS -> DEFAULT_MAX_ACCOUNT_FIELDS * improve "add field" button behavior * fix copy paste mistake in AccountFieldEditAdapter * refactor sendStatus to be a suspending function --- .../40.json | 929 ++++++++++++++++++ .../tusky/EditProfileActivity.kt | 25 +- .../tusky/adapter/AccountFieldEditAdapter.kt | 26 +- .../components/compose/ComposeActivity.kt | 111 ++- .../components/compose/ComposeViewModel.kt | 98 +- .../tusky/components/compose/MediaUploader.kt | 25 +- .../components/instanceinfo/InstanceInfo.kt | 9 +- .../instanceinfo/InstanceInfoRepository.kt | 25 +- .../keylesspalace/tusky/db/AppDatabase.java | 15 +- .../keylesspalace/tusky/db/InstanceEntity.kt | 18 +- .../com/keylesspalace/tusky/di/AppModule.kt | 2 +- .../keylesspalace/tusky/entity/Instance.kt | 33 +- .../keylesspalace/tusky/util/LiveDataUtil.kt | 98 -- .../tusky/viewmodel/EditProfileViewModel.kt | 31 +- app/src/main/res/layout/item_edit_field.xml | 38 +- .../tusky/ComposeActivityTest.kt | 44 +- 16 files changed, 1219 insertions(+), 308 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/40.json delete mode 100644 app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/40.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/40.json new file mode 100644 index 000000000..54d5a2bf8 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/40.json @@ -0,0 +1,929 @@ +{ + "formatVersion": 1, + "database": { + "version": 40, + "identityHash": "0423fb3f7d09db5f12023f2f4e7297b5", + "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)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `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, `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": "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, 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 + } + ], + "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, 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 + } + ], + "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, '0423fb3f7d09db5f12023f2f4e7297b5')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt index b749fe937..369f69262 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -28,6 +28,7 @@ import android.widget.ImageView import androidx.activity.viewModels import androidx.core.view.isVisible import androidx.lifecycle.LiveData +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy @@ -37,6 +38,7 @@ import com.canhub.cropper.CropImageContract import com.canhub.cropper.options import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory @@ -50,6 +52,7 @@ import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp +import kotlinx.coroutines.launch import javax.inject.Inject class EditProfileActivity : BaseActivity(), Injectable { @@ -58,8 +61,6 @@ class EditProfileActivity : BaseActivity(), Injectable { const val AVATAR_SIZE = 400 const val HEADER_WIDTH = 1500 const val HEADER_HEIGHT = 500 - - private const val MAX_ACCOUNT_FIELDS = 4 } @Inject @@ -71,6 +72,8 @@ class EditProfileActivity : BaseActivity(), Injectable { private val accountFieldEditAdapter = AccountFieldEditAdapter() + private var maxAccountFields = InstanceInfoRepository.DEFAULT_MAX_ACCOUNT_FIELDS + private enum class PickType { AVATAR, HEADER @@ -112,7 +115,7 @@ class EditProfileActivity : BaseActivity(), Injectable { binding.addFieldButton.setOnClickListener { accountFieldEditAdapter.addField() - if (accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) { + if (accountFieldEditAdapter.itemCount >= maxAccountFields) { it.isVisible = false } @@ -134,7 +137,8 @@ class EditProfileActivity : BaseActivity(), Injectable { binding.lockedCheckBox.isChecked = me.locked accountFieldEditAdapter.setFields(me.source?.fields ?: emptyList()) - binding.addFieldButton.isEnabled = me.source?.fields?.size ?: 0 < MAX_ACCOUNT_FIELDS + binding.addFieldButton.isVisible = + (me.source?.fields?.size ?: 0) < maxAccountFields if (viewModel.avatarData.value == null) { Glide.with(this) @@ -165,13 +169,12 @@ class EditProfileActivity : BaseActivity(), Injectable { } } - viewModel.obtainInstance() - viewModel.instanceData.observe(this) { result -> - if (result is Success) { - val instance = result.data - if (instance?.maxBioChars != null && instance.maxBioChars > 0) { - binding.noteEditTextLayout.counterMaxLength = instance.maxBioChars - } + lifecycleScope.launch { + viewModel.instanceData.collect { instanceInfo -> + maxAccountFields = instanceInfo.maxFields + accountFieldEditAdapter.setFieldLimits(instanceInfo.maxFieldNameLength, instanceInfo.maxFieldValueLength) + binding.addFieldButton.isVisible = + accountFieldEditAdapter.itemCount < maxAccountFields } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt index 7ba5537b8..30cf63097 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt @@ -27,6 +27,8 @@ import com.keylesspalace.tusky.util.BindingHolder class AccountFieldEditAdapter : RecyclerView.Adapter>() { private val fieldData = mutableListOf() + private var maxNameLength: Int? = null + private var maxValueLength: Int? = null fun setFields(fields: List) { fieldData.clear() @@ -41,6 +43,12 @@ class AccountFieldEditAdapter : RecyclerView.Adapter { return fieldData.map { StringField(it.first, it.second) @@ -60,10 +68,20 @@ class AccountFieldEditAdapter : RecyclerView.Adapter, position: Int) { - holder.binding.accountFieldName.setText(fieldData[position].first) - holder.binding.accountFieldValue.setText(fieldData[position].second) + holder.binding.accountFieldNameText.setText(fieldData[position].first) + holder.binding.accountFieldValueText.setText(fieldData[position].second) - holder.binding.accountFieldName.addTextChangedListener(object : TextWatcher { + holder.binding.accountFieldNameTextLayout.isCounterEnabled = maxNameLength != null + maxNameLength?.let { + holder.binding.accountFieldNameTextLayout.counterMaxLength = it + } + + holder.binding.accountFieldValueTextLayout.isCounterEnabled = maxValueLength != null + maxValueLength?.let { + holder.binding.accountFieldValueTextLayout.counterMaxLength = it + } + + holder.binding.accountFieldNameText.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(newText: Editable) { fieldData[holder.bindingAdapterPosition].first = newText.toString() } @@ -73,7 +91,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter if (success) { @@ -147,7 +145,7 @@ class ComposeActivity : } } private val pickMediaFile = registerForActivityResult(PickMediaFiles()) { uris -> - if (mediaCount + uris.size > maxUploadMediaNumber) { + if (viewModel.media.value.size + uris.size > maxUploadMediaNumber) { Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show() } else { uris.forEach { uri -> @@ -224,8 +222,8 @@ class ComposeActivity : binding.composeMediaPreviewBar.adapter = mediaAdapter binding.composeMediaPreviewBar.itemAnimator = null - subscribeToUpdates(mediaAdapter) setupButtons() + subscribeToUpdates(mediaAdapter) photoUploadUri = savedInstanceState?.getParcelable(PHOTO_UPLOAD_URI_KEY) @@ -363,36 +361,48 @@ class ComposeActivity : } private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter) { - withLifecycleContext { - viewModel.instanceInfo.observe { instanceData -> + lifecycleScope.launch { + viewModel.instanceInfo.collect { instanceData -> maximumTootCharacters = instanceData.maxChars charactersReservedPerUrl = instanceData.charactersReservedPerUrl + maxUploadMediaNumber = instanceData.maxMediaAttachments updateVisibleCharactersLeft() } - viewModel.emoji.observe { emoji -> setEmojiList(emoji) } - combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning -> + } + + lifecycleScope.launch { + viewModel.emoji.collect(::setEmojiList) + } + + lifecycleScope.launch { + viewModel.showContentWarning.combine(viewModel.markMediaAsSensitive) { showContentWarning, markSensitive -> updateSensitiveMediaToggle(markSensitive, showContentWarning) showContentWarning(showContentWarning) - }.subscribe() - viewModel.statusVisibility.observe { visibility -> - setStatusVisibility(visibility) - } - lifecycleScope.launch { - viewModel.media.collect { media -> - mediaAdapter.submitList(media) - if (media.size != mediaCount) { - mediaCount = media.size - binding.composeMediaPreviewBar.visible(media.isNotEmpty()) - updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false) - } - } - } + }.collect() + } - viewModel.poll.observe { poll -> + lifecycleScope.launch { + viewModel.statusVisibility.collect(::setStatusVisibility) + } + + lifecycleScope.launch { + viewModel.media.collect { media -> + mediaAdapter.submitList(media) + + binding.composeMediaPreviewBar.visible(media.isNotEmpty()) + updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value, viewModel.showContentWarning.value) + } + } + + lifecycleScope.launch { + viewModel.poll.collect { poll -> binding.pollPreview.visible(poll != null) poll?.let(binding.pollPreview::setPoll) } - viewModel.scheduledAt.observe { scheduledAt -> + } + + lifecycleScope.launch { + viewModel.scheduledAt.collect { scheduledAt -> if (scheduledAt == null) { binding.composeScheduleView.resetSchedule() } else { @@ -400,22 +410,30 @@ class ComposeActivity : } updateScheduleButton() } - combineOptionalLiveData(viewModel.media.asLiveData(), viewModel.poll) { media, poll -> + } + + lifecycleScope.launch { + viewModel.media.combine(viewModel.poll) { media, poll -> val active = poll == null && - media!!.size != 4 && + media.size < maxUploadMediaNumber && (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE) enableButton(binding.composeAddMediaButton, active, active) - enablePollButton(media.isNullOrEmpty()) - }.subscribe() - viewModel.uploadError.observe { throwable -> - Log.w(TAG, "media upload failed", throwable) + enablePollButton(media.isEmpty()) + }.collect() + } + + lifecycleScope.launch { + viewModel.uploadError.collect { throwable -> if (throwable is UploadServerError) { displayTransientError(throwable.errorMessage) } else { displayTransientError(R.string.error_media_upload_sending) } } - viewModel.setupComplete.observe { + } + + lifecycleScope.launch { + viewModel.setupComplete.collect { // Focus may have changed during view model setup, ensure initial focus is on the edit field binding.composeEditField.requestFocus() } @@ -711,13 +729,17 @@ class ComposeActivity : addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED } - private fun openPollDialog() { + private fun openPollDialog() = lifecycleScope.launch { addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED - val instanceParams = viewModel.instanceInfo.value!! + val instanceParams = viewModel.instanceInfo.first() showAddPollDialog( - this, viewModel.poll.value, instanceParams.pollMaxOptions, - instanceParams.pollMaxLength, instanceParams.pollMinDuration, instanceParams.pollMaxDuration, - viewModel::updatePoll + context = this@ComposeActivity, + poll = viewModel.poll.value, + maxOptionCount = instanceParams.pollMaxOptions, + maxOptionLength = instanceParams.pollMaxLength, + minDuration = instanceParams.pollMinDuration, + maxDuration = instanceParams.pollMaxDuration, + onUpdatePoll = viewModel::updatePoll ) } @@ -768,7 +790,7 @@ class ComposeActivity : } } var length = binding.composeEditField.length() - offset - if (viewModel.showContentWarning.value!!) { + if (viewModel.showContentWarning.value) { length += binding.composeContentWarningField.length() } return length @@ -822,7 +844,7 @@ class ComposeActivity : enableButtons(false) val contentText = binding.composeEditField.text.toString() var spoilerText = "" - if (viewModel.showContentWarning.value!!) { + if (viewModel.showContentWarning.value) { spoilerText = binding.composeContentWarningField.text.toString() } val characterCount = calculateTextLength() @@ -837,9 +859,8 @@ class ComposeActivity : ) } - viewModel.sendStatus(contentText, spoilerText).observe( - this - ) { + lifecycleScope.launch { + viewModel.sendStatus(contentText, spoilerText) finishingUploadDialog?.dismiss() deleteDraftAndFinish() } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index a7e1779cf..8829980b8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -18,10 +18,7 @@ package com.keylesspalace.tusky.components.compose import android.net.Uri import android.util.Log import androidx.core.net.toUri -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia @@ -38,30 +35,34 @@ import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.service.ServiceClient import com.keylesspalace.tusky.service.StatusToSend -import com.keylesspalace.tusky.util.combineLiveData import com.keylesspalace.tusky.util.randomAlphanumericString -import com.keylesspalace.tusky.util.toLiveData -import io.reactivex.rxjava3.core.Observable import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch -import kotlinx.coroutines.rx3.rxSingle import kotlinx.coroutines.withContext import javax.inject.Inject +@OptIn(FlowPreview::class) class ComposeViewModel @Inject constructor( private val api: MastodonApi, private val accountManager: AccountManager, private val mediaUploader: MediaUploader, private val serviceClient: ServiceClient, private val draftHelper: DraftHelper, - private val instanceInfoRepo: InstanceInfoRepository + instanceInfoRepo: InstanceInfoRepository ) : ViewModel() { private var replyingStatusAuthor: String? = null @@ -76,40 +77,32 @@ class ComposeViewModel @Inject constructor( private var contentWarningStateChanged: Boolean = false private var modifiedInitialState: Boolean = false - val instanceInfo: MutableLiveData = MutableLiveData() + val instanceInfo: SharedFlow = instanceInfoRepo::getInstanceInfo.asFlow() + .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) - val emoji: MutableLiveData?> = MutableLiveData() - val markMediaAsSensitive = - mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false) + val emoji: SharedFlow> = instanceInfoRepo::getEmojis.asFlow() + .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) - val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN) - val showContentWarning = mutableLiveData(false) - val setupComplete = mutableLiveData(false) - val poll: MutableLiveData = mutableLiveData(null) - val scheduledAt: MutableLiveData = mutableLiveData(null) + val markMediaAsSensitive: MutableStateFlow = + MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false) + + val statusVisibility: MutableStateFlow = MutableStateFlow(Status.Visibility.UNKNOWN) + val showContentWarning: MutableStateFlow = MutableStateFlow(false) + val setupComplete: MutableStateFlow = MutableStateFlow(false) + val poll: MutableStateFlow = MutableStateFlow(null) + val scheduledAt: MutableStateFlow = MutableStateFlow(null) val media: MutableStateFlow> = MutableStateFlow(emptyList()) - val uploadError = MutableLiveData() + val uploadError = MutableSharedFlow(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val mediaToJob = mutableMapOf() - private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty() - // Used in ComposeActivity to pass state to result function when cropImage contract inflight var cropImageItemOld: QueuedMedia? = null - init { - viewModelScope.launch { - emoji.postValue(instanceInfoRepo.getEmojis()) - } - viewModelScope.launch { - instanceInfo.postValue(instanceInfoRepo.getInstanceInfo()) - } - } - suspend fun pickMedia(mediaUri: Uri, description: String? = null): Result = withContext(Dispatchers.IO) { try { - val (type, uri, size) = mediaUploader.prepareMedia(mediaUri) + val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first()) val mediaItems = media.value if (type != QueuedMedia.Type.IMAGE && mediaItems.isNotEmpty() && @@ -157,10 +150,10 @@ class ComposeViewModel @Inject constructor( mediaToJob[mediaItem.localId] = viewModelScope.launch { mediaUploader - .uploadMedia(mediaItem) + .uploadMedia(mediaItem, instanceInfo.first()) .catch { error -> media.update { mediaValue -> mediaValue.filter { it.localId != mediaItem.localId } } - uploadError.postValue(error) + uploadError.emit(error) } .collect { event -> val item = media.value.find { it.localId == mediaItem.localId } @@ -216,7 +209,7 @@ class ComposeViewModel @Inject constructor( startingText?.startsWith(content.toString()) ?: false ) - val contentWarningChanged = showContentWarning.value!! && + val contentWarningChanged = showContentWarning.value && !contentWarning.isNullOrEmpty() && !startingContentWarning.startsWith(contentWarning.toString()) val mediaChanged = media.value.isNotEmpty() @@ -259,8 +252,8 @@ class ComposeViewModel @Inject constructor( inReplyToId = inReplyToId, content = content, contentWarning = contentWarning, - sensitive = markMediaAsSensitive.value!!, - visibility = statusVisibility.value!!, + sensitive = markMediaAsSensitive.value, + visibility = statusVisibility.value, mediaUris = mediaUris, mediaDescriptions = mediaDescriptions, poll = poll.value, @@ -271,38 +264,34 @@ class ComposeViewModel @Inject constructor( /** * Send status to the server. * Uses current state plus provided arguments. - * @return LiveData which will signal once the screen can be closed or null if there are errors */ - fun sendStatus( + suspend fun sendStatus( content: String, spoilerText: String - ): LiveData { + ) { - val deletionObservable = if (isEditingScheduledToot) { - rxSingle { api.deleteScheduledStatus(scheduledTootId.toString()) }.toObservable().map { } - } else { - Observable.just(Unit) - }.toLiveData() + if (!scheduledTootId.isNullOrEmpty()) { + api.deleteScheduledStatus(scheduledTootId!!) + } - val sendFlow = media + media .filter { items -> items.all { it.uploadPercent == -1 } } - .map { + .first { val mediaIds: MutableList = mutableListOf() val mediaUris: MutableList = mutableListOf() val mediaDescriptions: MutableList = mutableListOf() val mediaProcessed: MutableList = mutableListOf() - for (item in media.value) { + media.value.forEach { item -> mediaIds.add(item.id!!) mediaUris.add(item.uri) mediaDescriptions.add(item.description ?: "") mediaProcessed.add(false) } - val tootToSend = StatusToSend( text = content, warningText = spoilerText, - visibility = statusVisibility.value!!.serverString(), - sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!), + visibility = statusVisibility.value.serverString(), + sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value || showContentWarning.value), mediaIds = mediaIds, mediaUris = mediaUris.map { it.toString() }, mediaDescriptions = mediaDescriptions, @@ -319,9 +308,8 @@ class ComposeViewModel @Inject constructor( ) serviceClient.sendToot(tootToSend) + true } - - return combineLiveData(deletionObservable, sendFlow.asLiveData()) { _, _ -> } } suspend fun updateDescription(localId: Int, description: String): Boolean { @@ -369,7 +357,7 @@ class ComposeViewModel @Inject constructor( }) } ':' -> { - val emojiList = emoji.value ?: return emptyList() + val emojiList = emoji.replayCache.firstOrNull() ?: return emptyList() val incomplete = token.substring(1) return emojiList.filter { emoji -> @@ -389,7 +377,7 @@ class ComposeViewModel @Inject constructor( fun setup(composeOptions: ComposeActivity.ComposeOptions?) { - if (setupComplete.value == true) { + if (setupComplete.value) { return } @@ -476,8 +464,6 @@ class ComposeViewModel @Inject constructor( } } -fun mutableLiveData(default: T) = MutableLiveData().apply { value = default } - /** * Thrown when trying to add an image when video is already present or the other way around */ diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt index 324540d12..221b306e8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt @@ -27,6 +27,7 @@ import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo import com.keylesspalace.tusky.network.MediaUploadApi import com.keylesspalace.tusky.network.ProgressRequestBody import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN @@ -82,10 +83,10 @@ class MediaUploader @Inject constructor( ) { @OptIn(ExperimentalCoroutinesApi::class) - fun uploadMedia(media: QueuedMedia): Flow { + fun uploadMedia(media: QueuedMedia, instanceInfo: InstanceInfo): Flow { return flow { - if (shouldResizeMedia(media)) { - emit(downsize(media)) + if (shouldResizeMedia(media, instanceInfo)) { + emit(downsize(media, instanceInfo)) } else { emit(media) } @@ -94,7 +95,7 @@ class MediaUploader @Inject constructor( .flowOn(Dispatchers.IO) } - fun prepareMedia(inUri: Uri): PreparedMedia { + fun prepareMedia(inUri: Uri, instanceInfo: InstanceInfo): PreparedMedia { var mediaSize = MEDIA_SIZE_UNKNOWN var uri = inUri val mimeType: String? @@ -164,7 +165,7 @@ class MediaUploader @Inject constructor( if (mimeType != null) { return when (mimeType.substring(0, mimeType.indexOf('/'))) { "video" -> { - if (mediaSize > STATUS_VIDEO_SIZE_LIMIT) { + if (mediaSize > instanceInfo.videoSizeLimit) { throw VideoSizeException() } PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize) @@ -173,7 +174,7 @@ class MediaUploader @Inject constructor( PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize) } "audio" -> { - if (mediaSize > STATUS_AUDIO_SIZE_LIMIT) { + if (mediaSize > instanceInfo.videoSizeLimit) { throw AudioSizeException() } PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize) @@ -239,22 +240,18 @@ class MediaUploader @Inject constructor( } } - private fun downsize(media: QueuedMedia): QueuedMedia { + private fun downsize(media: QueuedMedia, instanceInfo: InstanceInfo): QueuedMedia { val file = createNewImageFile(context) - downsizeImage(media.uri, STATUS_IMAGE_SIZE_LIMIT, contentResolver, file) + downsizeImage(media.uri, instanceInfo.imageSizeLimit, contentResolver, file) return media.copy(uri = file.toUri(), mediaSize = file.length()) } - private fun shouldResizeMedia(media: QueuedMedia): Boolean { + private fun shouldResizeMedia(media: QueuedMedia, instanceInfo: InstanceInfo): Boolean { return media.type == QueuedMedia.Type.IMAGE && - (media.mediaSize > STATUS_IMAGE_SIZE_LIMIT || getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT) + (media.mediaSize > instanceInfo.imageSizeLimit || getImageSquarePixels(context.contentResolver, media.uri) > instanceInfo.imageMatrixLimit) } private companion object { private const val TAG = "MediaUploader" - private const val STATUS_VIDEO_SIZE_LIMIT = 41943040 // 40MiB - private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB - private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB - private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt index 05e10b6bc..bb97621c1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt @@ -21,5 +21,12 @@ data class InstanceInfo( val pollMaxLength: Int, val pollMinDuration: Int, val pollMaxDuration: Int, - val charactersReservedPerUrl: Int + val charactersReservedPerUrl: Int, + val videoSizeLimit: Int, + val imageSizeLimit: Int, + val imageMatrixLimit: Int, + val maxMediaAttachments: Int, + val maxFields: Int, + val maxFieldNameLength: Int?, + val maxFieldValueLength: Int? ) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt index 20c44ba47..7415dd06b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt @@ -69,7 +69,14 @@ class InstanceInfoRepository @Inject constructor( minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration, maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration, charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl, - version = instance.version + version = instance.version, + videoSizeLimit = instance.configuration?.mediaAttachments?.videoSizeLimit, + imageSizeLimit = instance.configuration?.mediaAttachments?.imageSizeLimit, + imageMatrixLimit = instance.configuration?.mediaAttachments?.imageMatrixLimit, + maxMediaAttachments = instance.configuration?.statuses?.maxMediaAttachments ?: instance.maxMediaAttachments, + maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields, + maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength, + maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength ) dao.insertOrReplace(instanceEntity) instanceEntity @@ -85,7 +92,14 @@ class InstanceInfoRepository @Inject constructor( pollMaxLength = instanceInfo?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH, pollMinDuration = instanceInfo?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION, pollMaxDuration = instanceInfo?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION, - charactersReservedPerUrl = instanceInfo?.charactersReservedPerUrl ?: DEFAULT_CHARACTERS_RESERVED_PER_URL + charactersReservedPerUrl = instanceInfo?.charactersReservedPerUrl ?: DEFAULT_CHARACTERS_RESERVED_PER_URL, + videoSizeLimit = instanceInfo?.videoSizeLimit ?: DEFAULT_VIDEO_SIZE_LIMIT, + imageSizeLimit = instanceInfo?.imageSizeLimit ?: DEFAULT_IMAGE_SIZE_LIMIT, + imageMatrixLimit = instanceInfo?.imageMatrixLimit ?: DEFAULT_IMAGE_MATRIX_LIMIT, + maxMediaAttachments = instanceInfo?.maxMediaAttachments ?: DEFAULT_MAX_MEDIA_ATTACHMENTS, + maxFields = instanceInfo?.maxFields ?: DEFAULT_MAX_ACCOUNT_FIELDS, + maxFieldNameLength = instanceInfo?.maxFieldNameLength, + maxFieldValueLength = instanceInfo?.maxFieldValueLength ) } } @@ -99,7 +113,14 @@ class InstanceInfoRepository @Inject constructor( private const val DEFAULT_MIN_POLL_DURATION = 300 private const val DEFAULT_MAX_POLL_DURATION = 604800 + private const val DEFAULT_VIDEO_SIZE_LIMIT = 41943040 // 40MiB + private const val DEFAULT_IMAGE_SIZE_LIMIT = 10485760 // 10MiB + private const val DEFAULT_IMAGE_MATRIX_LIMIT = 16777216 // 4096^2 Pixels + // Mastodon only counts URLs as this long in terms of status character limits const val DEFAULT_CHARACTERS_RESERVED_PER_URL = 23 + + const val DEFAULT_MAX_MEDIA_ATTACHMENTS = 4 + const val DEFAULT_MAX_ACCOUNT_FIELDS = 4 } } 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 c43b36524..e87758cef 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 = 39) + }, version = 40) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -581,4 +581,17 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `clientSecret` TEXT"); } }; + + public static final Migration MIGRATION_39_40 = new Migration(39, 40) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `videoSizeLimit` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `imageSizeLimit` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `imageMatrixLimit` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxMediaAttachments` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxFields` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxFieldNameLength` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxFieldValueLength` INTEGER"); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt index 01767f321..efcfe5278 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt @@ -31,7 +31,14 @@ data class InstanceEntity( val minPollDuration: Int?, val maxPollDuration: Int?, val charactersReservedPerUrl: Int?, - val version: String? + val version: String?, + val videoSizeLimit: Int?, + val imageSizeLimit: Int?, + val imageMatrixLimit: Int?, + val maxMediaAttachments: Int?, + val maxFields: Int?, + val maxFieldNameLength: Int?, + val maxFieldValueLength: Int? ) @TypeConverters(Converters::class) @@ -48,5 +55,12 @@ data class InstanceInfoEntity( val minPollDuration: Int?, val maxPollDuration: Int?, val charactersReservedPerUrl: Int?, - val version: String? + val version: String?, + val videoSizeLimit: Int?, + val imageSizeLimit: Int?, + val imageMatrixLimit: Int?, + val maxMediaAttachments: Int?, + val maxFields: Int?, + val maxFieldNameLength: Int?, + val maxFieldValueLength: Int? ) 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 e17cb3cf6..b92dcf15f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -65,7 +65,7 @@ class AppModule { AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32, 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_38_39, AppDatabase.MIGRATION_39_40 ) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt index 31fcca0e0..eccc86967 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt @@ -19,19 +19,20 @@ import com.google.gson.annotations.SerializedName data class Instance( val uri: String, - val title: String, - val description: String, - val email: String, + // val title: String, + // val description: String, + // val email: String, val version: String, - val urls: Map, - val stats: Map?, - val thumbnail: String?, - val languages: List, - @SerializedName("contact_account") val contactAccount: Account, + // val urls: Map, + // val stats: Map?, + // val thumbnail: String?, + // val languages: List, + // @SerializedName("contact_account") val contactAccount: Account, @SerializedName("max_toot_chars") val maxTootChars: Int?, - @SerializedName("max_bio_chars") val maxBioChars: Int?, @SerializedName("poll_limits") val pollConfiguration: PollConfiguration?, val configuration: InstanceConfiguration?, + @SerializedName("max_media_attachments") val maxMediaAttachments: Int?, + val pleroma: PleromaConfiguration? ) { override fun hashCode(): Int { return uri.hashCode() @@ -74,3 +75,17 @@ data class MediaAttachmentConfiguration( @SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int?, @SerializedName("video_matrix_limit") val videoMatrixLimit: Int?, ) + +data class PleromaConfiguration( + val metadata: PleromaMetadata? +) + +data class PleromaMetadata( + @SerializedName("fields_limits") val fieldLimits: PleromaFieldLimits +) + +data class PleromaFieldLimits( + @SerializedName("max_fields") val maxFields: Int?, + @SerializedName("name_length") val nameLength: Int?, + @SerializedName("value_length") val valueLength: Int? +) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt b/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt deleted file mode 100644 index 21c4307c6..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* Copyright 2019 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 androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.LiveDataReactiveStreams -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.Observer -import androidx.lifecycle.Transformations -import io.reactivex.rxjava3.core.BackpressureStrategy -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single - -inline fun LiveData.map(crossinline mapFunction: (X) -> Y): LiveData = - Transformations.map(this) { input -> mapFunction(input) } - -inline fun LiveData.switchMap( - crossinline switchMapFunction: (X) -> LiveData -): LiveData = Transformations.switchMap(this) { input -> switchMapFunction(input) } - -inline fun LiveData.filter(crossinline predicate: (X) -> Boolean): LiveData { - val liveData = MediatorLiveData() - liveData.addSource(this) { value -> - if (predicate(value)) { - liveData.value = value - } - } - return liveData -} - -fun LifecycleOwner.withLifecycleContext(body: LifecycleContext.() -> Unit) = - LifecycleContext(this).apply(body) - -class LifecycleContext(val lifecycleOwner: LifecycleOwner) { - inline fun LiveData.observe(crossinline observer: (T) -> Unit) = - this.observe(lifecycleOwner, Observer { observer(it) }) - - /** - * Just hold a subscription, - */ - fun LiveData.subscribe() = - this.observe(lifecycleOwner, Observer { }) -} - -/** - * Invokes @param [combiner] when value of both @param [a] and @param [b] are not null. Returns - * [LiveData] with value set to the result of calling [combiner] with value of both. - * Important! You still need to observe to the returned [LiveData] for [combiner] to be invoked. - */ -fun combineLiveData(a: LiveData, b: LiveData, combiner: (A, B) -> R): LiveData { - val liveData = MediatorLiveData() - liveData.addSource(a) { - if (a.value != null && b.value != null) { - liveData.value = combiner(a.value!!, b.value!!) - } - } - liveData.addSource(b) { - if (a.value != null && b.value != null) { - liveData.value = combiner(a.value!!, b.value!!) - } - } - return liveData -} - -/** - * Returns [LiveData] with value set to the result of calling [combiner] with value of [a] and [b] - * after either changes. Doesn't check if either has value. - * Important! You still need to observe to the returned [LiveData] for [combiner] to be invoked. - */ -fun combineOptionalLiveData(a: LiveData, b: LiveData, combiner: (A?, B?) -> R): LiveData { - val liveData = MediatorLiveData() - liveData.addSource(a) { - liveData.value = combiner(a.value, b.value) - } - liveData.addSource(b) { - liveData.value = combiner(a.value, b.value) - } - return liveData -} - -fun Single.toLiveData() = LiveDataReactiveStreams.fromPublisher(this.toFlowable()) -fun Observable.toLiveData( - backpressureStrategy: BackpressureStrategy = BackpressureStrategy.LATEST -) = LiveDataReactiveStreams.fromPublisher(this.toFlowable(BackpressureStrategy.LATEST)) diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt index bc7f435df..887766065 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -24,8 +24,9 @@ import androidx.lifecycle.viewModelScope import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.ProfileEditedEvent +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository import com.keylesspalace.tusky.entity.Account -import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.StringField import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.Error @@ -34,6 +35,11 @@ import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.getServerErrorMessage import com.keylesspalace.tusky.util.randomAlphanumericString +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody @@ -49,14 +55,18 @@ private const val AVATAR_FILE_NAME = "avatar.png" class EditProfileViewModel @Inject constructor( private val mastodonApi: MastodonApi, private val eventHub: EventHub, - private val application: Application + private val application: Application, + private val instanceInfoRepo: InstanceInfoRepository ) : ViewModel() { val profileData = MutableLiveData>() val avatarData = MutableLiveData() val headerData = MutableLiveData() val saveData = MutableLiveData>() - val instanceData = MutableLiveData>() + + @OptIn(FlowPreview::class) + val instanceData: Flow = instanceInfoRepo::getInstanceInfo.asFlow() + .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) private var oldProfileData: Account? = null @@ -186,19 +196,4 @@ class EditProfileViewModel @Inject constructor( private fun getCacheFileForName(filename: String): File { return File(application.cacheDir, filename) } - - fun obtainInstance() = viewModelScope.launch { - if (instanceData.value == null || instanceData.value is Error) { - instanceData.postValue(Loading()) - - mastodonApi.getInstance().fold( - { instance -> - instanceData.postValue(Success(instance)) - }, - { - instanceData.postValue(Error()) - } - ) - } - } } diff --git a/app/src/main/res/layout/item_edit_field.xml b/app/src/main/res/layout/item_edit_field.xml index 2777730bc..a658343e1 100644 --- a/app/src/main/res/layout/item_edit_field.xml +++ b/app/src/main/res/layout/item_edit_field.xml @@ -1,5 +1,6 @@ - + app:counterTextColor="?android:textColorTertiary"> - + + + + + app:counterTextColor="?android:textColorTertiary"> + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index ef863560d..f041f57b6 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -30,7 +30,6 @@ import com.keylesspalace.tusky.db.EmojisEntity import com.keylesspalace.tusky.db.InstanceDao import com.keylesspalace.tusky.db.InstanceInfoEntity import com.keylesspalace.tusky.di.ViewModelFactory -import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.InstanceConfiguration import com.keylesspalace.tusky.entity.StatusConfiguration @@ -48,8 +47,6 @@ import org.robolectric.Robolectric import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config import org.robolectric.fakes.RoboMenuItem -import java.util.Date -import kotlin.collections.HashMap /** * Created by charlag on 3/7/18. @@ -110,7 +107,7 @@ class ComposeActivityTest { val instanceDaoMock: InstanceDao = mock { onBlocking { getInstanceInfo(any()) } doReturn - InstanceInfoEntity(instanceDomain, null, null, null, null, null, null, null) + InstanceInfoEntity(instanceDomain, null, null, null, null, null, null, null, null, null, null, null, null, null, null) onBlocking { getEmojiInfo(any()) } doReturn EmojisEntity(instanceDomain, emptyList()) } @@ -461,38 +458,13 @@ class ComposeActivityTest { private fun getInstanceWithCustomConfiguration(maximumLegacyTootCharacters: Int? = null, configuration: InstanceConfiguration? = null): Instance { return Instance( - "https://example.token", - "Example dot Token", - "Example instance for testing", - "admin@example.token", - "2.6.3", - HashMap(), - null, - null, - listOf("en"), - Account( - id = "1", - localUsername = "admin", - username = "admin", - displayName = "admin", - createdAt = Date(), - note = "", - url = "https://example.token", - avatar = "", - header = "", - locked = false, - statusesCount = 0, - followersCount = 0, - followingCount = 0, - source = null, - bot = false, - emojis = emptyList(), - fields = emptyList(), - ), - maximumLegacyTootCharacters, - null, - null, - configuration, + uri = "https://example.token", + version = "2.6.3", + maxTootChars = maximumLegacyTootCharacters, + pollConfiguration = null, + configuration = configuration, + maxMediaAttachments = null, + pleroma = null ) } From 8b026991e0600c96e414119a4f56e1d1c85029c8 Mon Sep 17 00:00:00 2001 From: Martin Marconcini Date: Wed, 27 Jul 2022 21:06:51 +0200 Subject: [PATCH 004/142] 2616: Save Scheduled Time for Drafts. (#2624) * 2616: Save Scheduled Time for Drafts. Signed-off-by: Martin Marconcini * Revert 39.json schema to the original state before my changes. --- .../41.json | 935 ++++++++++++++++++ .../components/compose/ComposeViewModel.kt | 11 +- .../tusky/components/drafts/DraftHelper.kt | 6 +- .../tusky/components/drafts/DraftsActivity.kt | 6 +- .../keylesspalace/tusky/db/AppDatabase.java | 9 +- .../com/keylesspalace/tusky/db/DraftEntity.kt | 3 +- .../com/keylesspalace/tusky/di/AppModule.kt | 2 +- .../tusky/service/SendStatusService.kt | 3 +- 8 files changed, 965 insertions(+), 10 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/41.json diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/41.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/41.json new file mode 100644 index 000000000..2bc6256f1 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/41.json @@ -0,0 +1,935 @@ +{ + "formatVersion": 1, + "database": { + "version": 41, + "identityHash": "1de8f20c7f28e1f11b33e7a55137feef", + "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)", + "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 + } + ], + "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, `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": "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, 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 + } + ], + "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, 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 + } + ], + "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, '1de8f20c7f28e1f11b33e7a55137feef')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 8829980b8..95b6589c4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -76,6 +76,7 @@ class ComposeViewModel @Inject constructor( private var contentWarningStateChanged: Boolean = false private var modifiedInitialState: Boolean = false + private var hasScheduledTimeChanged: Boolean = false val instanceInfo: SharedFlow = instanceInfoRepo::getInstanceInfo.asFlow() .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) @@ -214,8 +215,9 @@ class ComposeViewModel @Inject constructor( !startingContentWarning.startsWith(contentWarning.toString()) val mediaChanged = media.value.isNotEmpty() val pollChanged = poll.value != null + val didScheduledTimeChange = hasScheduledTimeChanged - return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged + return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged || didScheduledTimeChange } fun contentWarningChanged(value: Boolean) { @@ -257,7 +259,8 @@ class ComposeViewModel @Inject constructor( mediaUris = mediaUris, mediaDescriptions = mediaDescriptions, poll = poll.value, - failedToSend = false + failedToSend = false, + scheduledAt = scheduledAt.value ) } @@ -456,6 +459,10 @@ class ComposeViewModel @Inject constructor( } fun updateScheduledAt(newScheduledAt: String?) { + if (newScheduledAt != scheduledAt.value) { + hasScheduledTimeChanged = true + } + scheduledAt.value = newScheduledAt } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt index a6cd3fcd7..45da941d6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt @@ -60,7 +60,8 @@ class DraftHelper @Inject constructor( mediaUris: List, mediaDescriptions: List, poll: NewPoll?, - failedToSend: Boolean + failedToSend: Boolean, + scheduledAt: String? ) = withContext(Dispatchers.IO) { val externalFilesDir = context.getExternalFilesDir("Tusky") @@ -116,7 +117,8 @@ class DraftHelper @Inject constructor( visibility = visibility, attachments = attachments, poll = poll, - failedToSend = failedToSend + failedToSend = failedToSend, + scheduledAt = scheduledAt ) draftDao.insertOrReplace(draft) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt index db6a8a313..deae29f4c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt @@ -106,7 +106,8 @@ class DraftsActivity : BaseActivity(), DraftActionListener { draftAttachments = draft.attachments, poll = draft.poll, sensitive = draft.sensitive, - visibility = draft.visibility + visibility = draft.visibility, + scheduledAt = draft.scheduledAt ) bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN @@ -143,7 +144,8 @@ class DraftsActivity : BaseActivity(), DraftActionListener { draftAttachments = draft.attachments, poll = draft.poll, sensitive = draft.sensitive, - visibility = draft.visibility + visibility = draft.visibility, + scheduledAt = draft.scheduledAt ) startActivity(ComposeActivity.startIntent(this, composeOptions)) 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 e87758cef..c4b672e4d 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 = 40) + }, version = 41) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -594,4 +594,11 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxFieldValueLength` INTEGER"); } }; + + public static final Migration MIGRATION_40_41 = new Migration(40, 41) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `DraftEntity` ADD COLUMN `scheduledAt` TEXT"); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt index a1e19c75c..41565b07c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt @@ -38,7 +38,8 @@ data class DraftEntity( val visibility: Status.Visibility, val attachments: List, val poll: NewPoll?, - val failedToSend: Boolean + val failedToSend: Boolean, + val scheduledAt: String?, ) /** 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 b92dcf15f..b2f7d7b7b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -65,7 +65,7 @@ class AppModule { AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32, 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_38_39, AppDatabase.MIGRATION_39_40, AppDatabase.MIGRATION_40_41, ) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt index 20ad8de8b..a6de4b3f6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt @@ -257,7 +257,8 @@ class SendStatusService : Service(), Injectable { mediaUris = status.mediaUris, mediaDescriptions = status.mediaDescriptions, poll = status.poll, - failedToSend = true + failedToSend = true, + scheduledAt = status.scheduledAt ) } From ec5e4a6c09786e90be3388d1ea516fba14c6f173 Mon Sep 17 00:00:00 2001 From: XoseM Date: Wed, 20 Jul 2022 13:27:00 +0000 Subject: [PATCH 005/142] Translated using Weblate (Galician) Currently translated at 100.0% (489 of 489 strings) Co-authored-by: XoseM Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/ Translation: Tusky/Tusky --- app/src/main/res/values-gl/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index af350478c..71273a3a4 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -133,7 +133,7 @@ Duración Enquisa Activar xestos de desprazamento para moverse entre lapelas - Motrar filtro das notificacións + Mostrar filtro das notificacións Fallou a busca Contas A conta pertence a outro servidor. Queres enviar unha copia anónima da denuncia alí tamén\? @@ -537,4 +537,6 @@ Gardando borrador… 1+ Editar imaxe + Fallou a carga dos detalles da conta + A imaxe non puido ser editada. \ No newline at end of file From ab1ac322f92f0cab8bd8a40b1071974e5bef6b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Al=C3=A9anor?= Date: Fri, 29 Jul 2022 20:27:04 +0000 Subject: [PATCH 006/142] Translated using Weblate (Finnish) Currently translated at 5.5% (1 of 18 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/fi/ --- fastlane/metadata/android/fi/title.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 fastlane/metadata/android/fi/title.txt diff --git a/fastlane/metadata/android/fi/title.txt b/fastlane/metadata/android/fi/title.txt new file mode 100644 index 000000000..0238ffc0a --- /dev/null +++ b/fastlane/metadata/android/fi/title.txt @@ -0,0 +1 @@ +Tusky From 2d2d7569e3dfe737f028861f04129fe5a7dfcec3 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Wed, 3 Aug 2022 17:23:54 +0200 Subject: [PATCH 007/142] fix compose field focus when replying to post with cw (#2634) --- .../tusky/components/compose/ComposeActivity.kt | 12 ++++-------- .../tusky/components/compose/ComposeViewModel.kt | 7 +++++-- 2 files changed, 9 insertions(+), 10 deletions(-) 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 e23f27766..7924c8e5e 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 @@ -247,7 +247,10 @@ class ComposeActivity : setupContentWarningField(composeOptions?.contentWarning) setupPollView() applyShareIntent(intent, savedInstanceState) - viewModel.setupComplete.value = true + + binding.composeEditField.post { + binding.composeEditField.requestFocus() + } } private fun applyShareIntent(intent: Intent, savedInstanceState: Bundle?) { @@ -431,13 +434,6 @@ class ComposeActivity : } } } - - lifecycleScope.launch { - viewModel.setupComplete.collect { - // Focus may have changed during view model setup, ensure initial focus is on the edit field - binding.composeEditField.requestFocus() - } - } } private fun setupButtons() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 95b6589c4..564c15515 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -89,7 +89,6 @@ class ComposeViewModel @Inject constructor( val statusVisibility: MutableStateFlow = MutableStateFlow(Status.Visibility.UNKNOWN) val showContentWarning: MutableStateFlow = MutableStateFlow(false) - val setupComplete: MutableStateFlow = MutableStateFlow(false) val poll: MutableStateFlow = MutableStateFlow(null) val scheduledAt: MutableStateFlow = MutableStateFlow(null) @@ -101,6 +100,8 @@ class ComposeViewModel @Inject constructor( // Used in ComposeActivity to pass state to result function when cropImage contract inflight var cropImageItemOld: QueuedMedia? = null + private var setupComplete = false + suspend fun pickMedia(mediaUri: Uri, description: String? = null): Result = withContext(Dispatchers.IO) { try { val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first()) @@ -380,7 +381,7 @@ class ComposeViewModel @Inject constructor( fun setup(composeOptions: ComposeActivity.ComposeOptions?) { - if (setupComplete.value) { + if (setupComplete) { return } @@ -452,6 +453,8 @@ class ComposeViewModel @Inject constructor( } replyingStatusContent = composeOptions?.replyingStatusContent replyingStatusAuthor = composeOptions?.replyingStatusAuthor + + setupComplete = true } fun updatePoll(newPoll: NewPoll) { From 55796c9a305c63f082382d2bc533560e6fd693bf Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 4 Aug 2022 16:48:26 +0200 Subject: [PATCH 008/142] update minSdkVersion to 23 (#2638) closes #2606 --- app/build.gradle | 2 +- app/src/main/AndroidManifest.xml | 3 --- .../java/com/keylesspalace/tusky/MainActivity.kt | 3 +-- .../tusky/adapter/NotificationsAdapter.java | 2 +- .../com/keylesspalace/tusky/adapter/PollAdapter.kt | 3 +-- .../tusky/components/account/AccountActivity.kt | 3 +-- .../tusky/components/compose/ComposeActivity.kt | 10 +++++----- .../components/compose/view/ProgressImageView.java | 8 +++----- .../notifications/NotificationHelper.java | 2 +- .../com/keylesspalace/tusky/fragment/SFragment.java | 2 +- .../tusky/receiver/SendStatusBroadcastReceiver.kt | 13 ++----------- .../tusky/service/SendStatusService.kt | 12 +++--------- .../keylesspalace/tusky/util/StatusViewHelper.kt | 3 +-- 13 files changed, 21 insertions(+), 45 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 981ef3ef8..730d428a9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,7 +22,7 @@ android { compileSdkVersion 31 defaultConfig { applicationId APP_ID - minSdkVersion 21 + minSdkVersion 23 targetSdkVersion 31 versionCode 94 versionName "19.0" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 48a5e622f..2273316d8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,9 +7,6 @@ - >() { } resultTextView.background.level = level - resultTextView.background.setTint(ContextCompat.getColor(resultTextView.context, optionColor)) + resultTextView.background.setTint(resultTextView.context.getColor(optionColor)) resultTextView.setOnClickListener(resultClickListener) } SINGLE -> { 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 7b5b8d7d2..6c8f997be 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 @@ -31,7 +31,6 @@ import androidx.annotation.ColorInt import androidx.annotation.Px import androidx.appcompat.app.AlertDialog import androidx.core.app.ActivityOptionsCompat -import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat @@ -171,7 +170,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI */ private fun loadResources() { toolbarColor = ThemeUtils.getColor(this, R.attr.colorSurface) - statusBarColorTransparent = ContextCompat.getColor(this, R.color.transparent_statusbar_background) + statusBarColorTransparent = getColor(R.color.transparent_statusbar_background) statusBarColorOpaque = ThemeUtils.getColor(this, R.attr.colorPrimaryDark) avatarSize = resources.getDimension(R.dimen.account_activity_avatar_size) titleVisibleHeight = resources.getDimensionPixelSize(R.dimen.account_activity_scroll_title_visible_height) 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 7924c8e5e..0ffe8f600 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 @@ -595,12 +595,12 @@ class ComposeActivity : @ColorInt val color = if (contentWarningShown) { binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) binding.composeHideMediaButton.isClickable = false - ContextCompat.getColor(this, R.color.transparent_tusky_blue) + getColor(R.color.transparent_tusky_blue) } else { binding.composeHideMediaButton.isClickable = true if (markMediaSensitive) { binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) - ContextCompat.getColor(this, R.color.tusky_blue) + getColor(R.color.tusky_blue) } else { binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp) ThemeUtils.getColor(this, android.R.attr.textColorTertiary) @@ -614,7 +614,7 @@ class ComposeActivity : @ColorInt val color = if (binding.composeScheduleView.time == null) { ThemeUtils.getColor(this, android.R.attr.textColorTertiary) } else { - ContextCompat.getColor(this, R.color.tusky_blue) + getColor(R.color.tusky_blue) } binding.composeScheduleButton.drawable.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) } @@ -797,7 +797,7 @@ class ComposeActivity : binding.composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength) val textColor = if (remainingLength < 0) { - ContextCompat.getColor(this, R.color.tusky_red) + getColor(R.color.tusky_red) } else { ThemeUtils.getColor(this, android.R.attr.textColorTertiary) } @@ -969,7 +969,7 @@ class ComposeActivity : binding.composeContentWarningBar.show() binding.composeContentWarningField.setSelection(binding.composeContentWarningField.text.length) binding.composeContentWarningField.requestFocus() - ContextCompat.getColor(this, R.color.tusky_blue) + getColor(R.color.tusky_blue) } else { binding.composeContentWarningBar.hide() binding.composeEditField.requestFocus() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.java index fde993d1e..335196104 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.java @@ -58,15 +58,14 @@ public final class ProgressImageView extends AppCompatImageView { } private void init() { - circlePaint.setColor(ContextCompat.getColor(getContext(), R.color.tusky_blue)); + circlePaint.setColor(getContext().getColor(R.color.tusky_blue)); circlePaint.setStrokeWidth(Utils.dpToPx(getContext(), 4)); circlePaint.setStyle(Paint.Style.STROKE); clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT)); markBgPaint.setStyle(Paint.Style.FILL); - markBgPaint.setColor(ContextCompat.getColor(getContext(), - R.color.tusky_grey_10)); + markBgPaint.setColor(getContext().getColor(R.color.tusky_grey_10)); captionDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.spellcheck); } @@ -81,8 +80,7 @@ public final class ProgressImageView extends AppCompatImageView { } public void setChecked(boolean checked) { - this.markBgPaint.setColor(ContextCompat.getColor(getContext(), - checked ? R.color.tusky_blue : R.color.tusky_grey_10)); + this.markBgPaint.setColor(getContext().getColor(checked ? R.color.tusky_blue : R.color.tusky_grey_10)); invalidate(); } 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 45ecd0f65..bb1599fb6 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 @@ -296,7 +296,7 @@ public class NotificationHelper { .setSmallIcon(R.drawable.ic_notify) .setContentIntent(summary ? summaryResultPendingIntent : eventResultPendingIntent) .setDeleteIntent(deletePendingIntent) - .setColor(ContextCompat.getColor(context, R.color.notification_color)) + .setColor(context.getColor(R.color.notification_color)) .setGroup(account.getAccountId()) .setAutoCancel(true) .setShortcutId(Long.toString(account.getId())) 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 01a08c203..fe94dbf8d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -354,7 +354,7 @@ public abstract class SFragment extends Fragment implements Injectable { urlIndex); if (view != null) { String url = active.getAttachment().getUrl(); - ViewCompat.setTransitionName(view, url); + view.setTransitionName(url); ActivityOptionsCompat options = ActivityOptionsCompat.makeSceneTransitionAnimation(getActivity(), view, url); diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt index a0eac8334..53cccf09d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -18,13 +18,10 @@ package com.keylesspalace.tusky.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.graphics.Color import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.app.RemoteInput -import androidx.core.content.ContextCompat -import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager @@ -66,7 +63,7 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_MENTION + senderIdentifier) .setSmallIcon(R.drawable.ic_notify) - .setColor(ContextCompat.getColor(context, R.color.tusky_blue)) + .setColor(context.getColor(R.color.tusky_blue)) .setGroup(senderFullName) .setDefaults(0) // So it doesn't ring twice, notify only in Target callback @@ -107,15 +104,9 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { context.startService(sendIntent) - val color = if (BuildConfig.FLAVOR == "green") { - Color.parseColor("#19A341") - } else { - ContextCompat.getColor(context, R.color.tusky_blue) - } - val builder = NotificationCompat.Builder(context, NotificationHelper.CHANNEL_MENTION + senderIdentifier) .setSmallIcon(R.drawable.ic_notify) - .setColor(color) + .setColor(context.getColor(R.color.notification_color)) .setGroup(senderFullName) .setDefaults(0) // So it doesn't ring twice, notify only in Target callback diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt index a6de4b3f6..13af53ec6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt @@ -14,7 +14,6 @@ import android.os.Parcelable import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat -import androidx.core.content.ContextCompat import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.R import com.keylesspalace.tusky.appstore.EventHub @@ -88,7 +87,7 @@ class SendStatusService : Service(), Injectable { .setContentText(notificationText) .setProgress(1, 0, true) .setOngoing(true) - .setColor(ContextCompat.getColor(this, R.color.notification_color)) + .setColor(getColor(R.color.notification_color)) .addAction(0, getString(android.R.string.cancel), cancelSendingIntent(sendingNotificationId)) if (statusesToSend.size == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -189,12 +188,7 @@ class SendStatusService : Service(), Injectable { .setSmallIcon(R.drawable.ic_notify) .setContentTitle(getString(R.string.send_post_notification_error_title)) .setContentText(getString(R.string.send_post_notification_saved_content)) - .setColor( - ContextCompat.getColor( - this@SendStatusService, - R.color.notification_color - ) - ) + .setColor(getColor(R.color.notification_color)) notificationManager.cancel(statusId) notificationManager.notify(errorNotificationId--, builder.build()) @@ -237,7 +231,7 @@ class SendStatusService : Service(), Injectable { .setSmallIcon(R.drawable.ic_notify) .setContentTitle(getString(R.string.send_post_notification_cancel_title)) .setContentText(getString(R.string.send_post_notification_saved_content)) - .setColor(ContextCompat.getColor(this@SendStatusService, R.color.notification_color)) + .setColor(getColor(R.color.notification_color)) notificationManager.notify(statusId, builder.build()) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt index 0752c4e5c..44d9fee3b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt @@ -23,7 +23,6 @@ import android.view.View import android.widget.ImageView import android.widget.TextView import androidx.annotation.DrawableRes -import androidx.core.content.ContextCompat import com.bumptech.glide.Glide import com.keylesspalace.tusky.R import com.keylesspalace.tusky.entity.Attachment @@ -319,7 +318,7 @@ class StatusViewHelper(private val itemView: View) { } pollResults[i].background.level = level - pollResults[i].background.setTint(ContextCompat.getColor(pollResults[i].context, optionColor)) + pollResults[i].background.setTint(pollResults[i].context.getColor(optionColor)) } else { pollResults[i].visibility = View.GONE } From 119693bc3c33984d8f8eccb8df446e6c67dafeaf Mon Sep 17 00:00:00 2001 From: Konstantin Date: Thu, 4 Aug 2022 15:14:08 +0000 Subject: [PATCH 009/142] Translated using Weblate (German) Currently translated at 100.0% (489 of 489 strings) Co-authored-by: Konstantin Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/ Translation: Tusky/Tusky --- app/src/main/res/values-de/strings.xml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 6be0be581..09e78162a 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -541,4 +541,11 @@ Du hast dich erneut in dein aktuelles Konto eingeloggt, um Tusky die Genehmigung für Push-Abonnements zu erteilen. Du hast jedoch noch andere Konten, die nicht auf diese Weise migriert wurden. Wechsel zu diesen Konten und melde dich nacheinander neu an, um die Unterstützung für UnifiedPush-Benachrichtigungen zu aktivieren. Um Push-Benachrichtigungen über UnifiedPush verwenden zu können, benötigt Tusky die Erlaubnis, Benachrichtigungen auf Ihrem Mastodon-Server zu abonnieren. Dies erfordert eine erneute Anmeldung, um die Tusky gewährten OAuth-Bereiche zu ändern. Wenn du die Option zum erneuten Einloggen hier oder in den Kontoeinstellungen verwendest, bleiben alle deine lokalen Entwürfe und der Cache erhalten. Melde alle Konten neu an, um die Unterstützung für Push-Benachrichtigungen zu aktivieren. + Beigetreten im %1$s + 1+ + Fehler beim laden der Kontodetails + Bild bearbeiten + Details + Das Bild konnte nicht bearbeitet werden. + Speichere den Entwurf… \ No newline at end of file From f1556ef97e8f698efca4f752c215101ceb78b85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laura=20Al=C3=A9anor?= Date: Thu, 4 Aug 2022 15:14:08 +0000 Subject: [PATCH 010/142] Translated using Weblate (Finnish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 51.9% (254 of 489 strings) Co-authored-by: Laura Aléanor Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fi/ Translation: Tusky/Tusky --- app/src/main/res/values-fi/strings.xml | 123 ++++++++++++++++++++++--- 1 file changed, 112 insertions(+), 11 deletions(-) diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index cd3ada402..2b7c3a7c6 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -21,7 +21,7 @@ 1 tunti 30 minuuttia 5 minuuttia - Lisää hashtag + Lisää aihetunniste Ei kuvausta CC-BY-SA 4.0 CC-BY 4.0 @@ -37,10 +37,10 @@ Tuskyn profiili Tusky %s Lukittu tili - Uusia tuuttauksia + Uudet tuuttaukset Seuraamispyynnöt - Uusia seuraajia - Uusia mainintoja + Uudet seuraajat + Uudet maininnat HTTP-välityspalvelin Näytä vastaukset Teema @@ -83,7 +83,7 @@ suljettu Suodatin Lista - Hastagit + Aihetunnisteet Julkinen Kiinnitä Poista kiinnitys @@ -93,7 +93,7 @@ Listat Päivitä Poista - Audio + Ääni Video Kuvat Tietoja @@ -119,14 +119,14 @@ Kuvaus Linkit Maininnat - Hastagit - Hastagit + Aihetunnisteet + Aihetunnisteet Maininnat Linkit Nollaa Luonnokset Hae - Älä hyväksy + Hylkää Hyväksy Peruuta Muokkaa @@ -142,7 +142,7 @@ Profiili Sulje Yritä uudelleen - TUUTTAUS + TUUT Poista Muokkaa Ilmianna @@ -151,7 +151,7 @@ Seurataan Seuraa TUUTAA! - Tuuttaus + Lanka Ajastetut tuuttaukset Vastaa \@%s @@ -167,4 +167,105 @@ Paikallinen Ilmoitukset Koti + Poista suosikki + Avaa jakajan tili + Muokkaa kuvaa + Mykistetyt tilit + %s seurasi sinua + + %d uusi kannsakäyminen + %d uutta kanssakäymistä + + Poista ja kirjoita uudelleen + Samaan julkaisuun ei voi liittää sekä kuvia että videoita. + %1$s, %2$s, ja %3$s + Poista lista + Muuta listan nimeä + Tällaista tiedostoa ei voida ladata ylös. + %s haluaa seurata sinua + Verkostoitu + Lähetys epäonnistui. + Ilmianna @%s + Lisähuomautuksia\? + Tilitietojen lataaminen epäonnistui + Tiedostojen lähettäminen vaatii lukuoikeuden. + Aseta kuvaus + Kirjanmerkki + Poista mykistys verkkonimeltä %s + Avaa tilinä %s + Enemmän + %s on maininnut sinut + Tiedoston lataaminen epäonnistui. + Poista ilmoitusten mykistys tililtä %s + %s jakoi julkaisusi + Julkaisusi on liian pitkä! + Poista keskustelun mykistys + Ladataan kuvaa %1$s + Kuvauksen lisääminen epäonnistui + Mykistä keskustelu + On syntynyt virhe. + Halutako varmasti kirjautua ulos tililtä %s1\? + Poista jako + Luo lista + Mykistä %s + Lisää tili listalle + Piilotetut verkkonimet + %s lisäsi julkaisusi suosikkeihinsa + Näytä jaot + Näytä jaetut julkaisut + Muokkaa listaa + %1$s, %2$s, %3$s ja %4$d muuta + + Kuvaa näkövammaisille +\n(enintään %d merkkiä) + + + Aihetunniste ilman #-merkkiä + Piilotetus verkkonimet + Jaa… + Verkkoselainta ei löytynyt. + Poista keskustelu + Poista tili listalta + %s julkaisi juuri + 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! + Avaa media No. %d + Julkaisun lähetys epäonnistui. + Tätä kenttää ei voi jättää tyhjäksi. + Tiedotukset + Ilmoitukset seuraamiesi uusista julkaisuista + Lisää suosikiksi + Tiedostojen tallentamiseen vaaditaan kirjoitusoikeus. + %1$s ja %2$s + Syöttämäsi verkkonimi on virheellinen + Poista mykistys tililtä %s + Kirjoita julkaisu + Hae seuraamiasi henkilöitä + Täällä ei ole mitään. + %s rekisteröityi + Rekisteröitymiset + Ilmoitukset uusista käyttäkistä + Sisäänkirjautuminen + Poista kirjanmerkki + 90 päivää + 180 päivää + 365 päivää + Sisäänkirjautumissivun lataaminen epäonnistui. + Jaa + Julkaisun näkyvyys + Vastauksetkin + %s jakoi + Herkkää sisältoä + Klikkaa näyttääksesi + Laajenna + Vähennä + Jaa julkaisun URL… + %s muokkasi julkaisua + Julkaisujen muokkaukset + Kirjaudu uudestaan sisään ottaaksesi vastaan push-ilmoituksia + Mykistä ilmoitukset tililtä %s + Yksityiskohdat + Kuvaa ei voitu muokata. \ No newline at end of file From afa92a7ca1a360d21312673d3a6eba4bb8488ca0 Mon Sep 17 00:00:00 2001 From: knuxify Date: Thu, 4 Aug 2022 15:14:08 +0000 Subject: [PATCH 011/142] Translated using Weblate (Polish) Currently translated at 100.0% (489 of 489 strings) Co-authored-by: knuxify Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pl/ Translation: Tusky/Tusky --- app/src/main/res/values-pl/strings.xml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 044c57ed4..2cf8bfad3 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -558,4 +558,15 @@ Edycje wpisów Zapisywanie szkicu… Nie można załadować strony logowania. + Zalogowałeś/-aś się ponownie na swoje konto, aby przyzwolić Tusky na wysyłanie powiadomień push. Masz jednak inne konta które nie zostały zmigrowane. Przełącz się na nie i zaloguj się ponownie aby włączyć wsparcie dla powiadomień UnifiedPush. + 1+ + Dołączył/-a %1$s + Zaloguj się ponownie na wszystkie konta aby włączyć wsparcie dla powiadomień push. + Aby użyć powiadomień push przez UnifiedPush, Tusky wymaga pozwolenia na subskrybowanie powiadomień na twoim serwerze Mastodon. Wymaga to ponownego zalogowania aby zmienić zakresy OAuth przyznane Tusky. Użycie opcji ponownego zalogowania tutaj lub w ustawieniach konta zachowa wszystkie szkice i pamięć podręczną. + Edytuj obraz + Obrazek nie mógł być zmodyfikowany. + Zaloguj się ponownie aby włączyć powiadomienia push + Odrzuć + Detale + Ładowanie informacji o koncie nie powiodło się \ No newline at end of file From df9e2652e95078b7a251b40c274bf5ffc17cbbd3 Mon Sep 17 00:00:00 2001 From: Bruno Miguel Date: Thu, 4 Aug 2022 15:14:08 +0000 Subject: [PATCH 012/142] Translated using Weblate (Portuguese (Portugal)) Currently translated at 100.0% (489 of 489 strings) Co-authored-by: Bruno Miguel Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pt_PT/ Translation: Tusky/Tusky --- app/src/main/res/values-pt-rPT/strings.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index e7d565590..87dd3ca59 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -529,4 +529,16 @@ Aceitar Rejeitar Não foi possível carregar a página de login + Erro ao carregar os detalhes da conta + Faz novamente login em todas as contas para ativar as notificações push. + Criada em %1$s + Para ativar as notificações push através de UnifiedPush, o Tusky necessita de permissão para subscrever as notificações da tua instância Mastodon. Isto obriga a fazer login novamente, por forma a alterar o escopo das permissões fornecidas ao Tusky pelo OAuth. Usar a opção de novo login, aqui ou nas Configurações da Conta, preservará todos os teus rascunhos e cache locais. + 1+ + Fizeste novo login na tua conta para dar permissão para a subscrição das notificações push no Tusky. Contudo, ainda tens outras contas sem esta permissão. Podes atribuir essa permissão fazendo novo login em cada uma delas e ativar o suporte para UnifiedPush. + Editar imagem + Não foi possível editar a imagem. + A guardar rascunho… + Faz novamente login para as notificações push + Descartar + Detalhes \ No newline at end of file From 17fb93626cb168af759582a615bf6b23a34c5d31 Mon Sep 17 00:00:00 2001 From: QuirkyPony <43992380+QuirkyPony@users.noreply.github.com> Date: Fri, 5 Aug 2022 18:55:13 +0200 Subject: [PATCH 013/142] adapt file size error messages to show real instance upload limit (#2630) * remove megabyte counts from file size error messages The file size limits depend on the server; change strings to reflect that. * show real file size limits instead of assuming Mastodon defaults * correct previous commit * correct previous commit (again) * remove megabyte counts from file size error messages The file size limits depend on the server; change strings to reflect that. * Translated using Weblate (Galician) Currently translated at 100.0% (489 of 489 strings) Co-authored-by: XoseM Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gl/ Translation: Tusky/Tusky * Translated using Weblate (Finnish) Currently translated at 5.5% (1 of 18 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/fi/ * correct previous commit correct previous commit (again) fixed type error caused by previous commits * fix lint error... * Update strings.xml * improve code, calculate correct max size and format it Co-authored-by: Laura Co-authored-by: XoseM Co-authored-by: Konrad Pozniak --- .../tusky/components/compose/ComposeActivity.kt | 17 +++++++++++------ .../tusky/components/compose/MediaUploader.kt | 7 +++---- app/src/main/res/values/strings.xml | 5 ++--- 3 files changed, 16 insertions(+), 13 deletions(-) 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 0ffe8f600..832eee813 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 @@ -103,6 +103,7 @@ import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize import java.io.File import java.io.IOException +import java.text.DecimalFormat import java.util.Locale import javax.inject.Inject import kotlin.math.max @@ -952,13 +953,17 @@ class ComposeActivity : private fun pickMedia(uri: Uri) { lifecycleScope.launch { viewModel.pickMedia(uri).onFailure { throwable -> - val errorId = when (throwable) { - is VideoSizeException -> R.string.error_video_upload_size - is AudioSizeException -> R.string.error_audio_upload_size - is VideoOrImageException -> R.string.error_media_upload_image_or_video - else -> R.string.error_media_upload_opening + val errorString = when (throwable) { + is FileSizeException -> { + val decimalFormat = DecimalFormat("0.##") + val allowedSizeInMb = throwable.allowedSizeInBytes.toDouble() / (1024 * 1024) + val formattedSize = decimalFormat.format(allowedSizeInMb) + getString(R.string.error_multimedia_size_limit, formattedSize) + } + is VideoOrImageException -> getString(R.string.error_media_upload_image_or_video) + else -> getString(R.string.error_media_upload_opening) } - displayTransientError(errorId) + displayTransientError(errorString) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt index 221b306e8..d7769cbd7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt @@ -71,8 +71,7 @@ fun createNewImageFile(context: Context, suffix: String = ".jpg"): File { data class PreparedMedia(val type: QueuedMedia.Type, val uri: Uri, val size: Long) -class AudioSizeException : Exception() -class VideoSizeException : Exception() +class FileSizeException(val allowedSizeInBytes: Int) : Exception() class MediaTypeException : Exception() class CouldNotOpenFileException : Exception() class UploadServerError(val errorMessage: String) : Exception() @@ -166,7 +165,7 @@ class MediaUploader @Inject constructor( return when (mimeType.substring(0, mimeType.indexOf('/'))) { "video" -> { if (mediaSize > instanceInfo.videoSizeLimit) { - throw VideoSizeException() + throw FileSizeException(instanceInfo.videoSizeLimit) } PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize) } @@ -175,7 +174,7 @@ class MediaUploader @Inject constructor( } "audio" -> { if (mediaSize > instanceInfo.videoSizeLimit) { - throw AudioSizeException() + throw FileSizeException(instanceInfo.videoSizeLimit) } PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1a7ef2983..7c163d188 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -12,9 +12,8 @@ Failed loading account details Could not load the login page. The post is too long! - The file must be less than 8MB. - Video files must be less than 40MB. - Audio files must be less than 40MB. + Video and audio files cannot exceed %s MB in size. + The image could not be edited. That type of file cannot be uploaded. That file could not be opened. From 69e43d718fd543dd558902ae4f6e1a9d0c36dc5b Mon Sep 17 00:00:00 2001 From: Connyduck Date: Fri, 5 Aug 2022 16:55:17 +0000 Subject: [PATCH 014/142] Translated using Weblate (German) Currently translated at 100.0% (489 of 489 strings) Translated using Weblate (German) Currently translated at 100.0% (489 of 489 strings) Co-authored-by: Connyduck Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/ Translation: Tusky/Tusky --- app/src/main/res/values-de/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 09e78162a..7c181ae4e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -541,9 +541,9 @@ Du hast dich erneut in dein aktuelles Konto eingeloggt, um Tusky die Genehmigung für Push-Abonnements zu erteilen. Du hast jedoch noch andere Konten, die nicht auf diese Weise migriert wurden. Wechsel zu diesen Konten und melde dich nacheinander neu an, um die Unterstützung für UnifiedPush-Benachrichtigungen zu aktivieren. Um Push-Benachrichtigungen über UnifiedPush verwenden zu können, benötigt Tusky die Erlaubnis, Benachrichtigungen auf Ihrem Mastodon-Server zu abonnieren. Dies erfordert eine erneute Anmeldung, um die Tusky gewährten OAuth-Bereiche zu ändern. Wenn du die Option zum erneuten Einloggen hier oder in den Kontoeinstellungen verwendest, bleiben alle deine lokalen Entwürfe und der Cache erhalten. Melde alle Konten neu an, um die Unterstützung für Push-Benachrichtigungen zu aktivieren. - Beigetreten im %1$s + %1$s beigetreten 1+ - Fehler beim laden der Kontodetails + Fehler beim Laden der Kontodetails Bild bearbeiten Details Das Bild konnte nicht bearbeitet werden. From 5e425cdaf0e335ee3c1c7883e3567503fc9ade58 Mon Sep 17 00:00:00 2001 From: Weblate Date: Fri, 5 Aug 2022 16:55:17 +0000 Subject: [PATCH 015/142] Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Weblate Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ Translation: Tusky/Tusky --- app/src/main/res/values-ar/strings.xml | 2 -- app/src/main/res/values-bg/strings.xml | 2 -- app/src/main/res/values-bn-rBD/strings.xml | 2 -- app/src/main/res/values-bn-rIN/strings.xml | 2 -- app/src/main/res/values-ca/strings.xml | 3 --- app/src/main/res/values-ckb/strings.xml | 2 -- app/src/main/res/values-cs/strings.xml | 2 -- app/src/main/res/values-cy/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 2 -- app/src/main/res/values-el/strings.xml | 2 -- app/src/main/res/values-en-rGB/strings.xml | 3 --- app/src/main/res/values-eo/strings.xml | 2 -- app/src/main/res/values-es/strings.xml | 2 -- app/src/main/res/values-eu/strings.xml | 2 -- app/src/main/res/values-fa/strings.xml | 2 -- app/src/main/res/values-fr/strings.xml | 2 -- app/src/main/res/values-fy/strings.xml | 2 -- app/src/main/res/values-ga/strings.xml | 2 -- app/src/main/res/values-gd/strings.xml | 2 -- app/src/main/res/values-gl/strings.xml | 2 -- app/src/main/res/values-hi/strings.xml | 3 --- app/src/main/res/values-hu/strings.xml | 2 -- app/src/main/res/values-is/strings.xml | 2 -- app/src/main/res/values-it/strings.xml | 2 -- app/src/main/res/values-ja/strings.xml | 2 -- app/src/main/res/values-ko/strings.xml | 1 - app/src/main/res/values-ml/strings.xml | 1 - app/src/main/res/values-nl/strings.xml | 2 -- app/src/main/res/values-no-rNB/strings.xml | 2 -- app/src/main/res/values-oc/strings.xml | 2 -- app/src/main/res/values-pl/strings.xml | 3 --- app/src/main/res/values-pt-rBR/strings.xml | 2 -- app/src/main/res/values-pt-rPT/strings.xml | 2 -- app/src/main/res/values-ru/strings.xml | 2 -- app/src/main/res/values-sa/strings.xml | 2 -- app/src/main/res/values-sk/strings.xml | 2 -- app/src/main/res/values-sl/strings.xml | 1 - app/src/main/res/values-sv/strings.xml | 2 -- app/src/main/res/values-ta/strings.xml | 2 -- app/src/main/res/values-th/strings.xml | 4 +--- app/src/main/res/values-tr/strings.xml | 2 -- app/src/main/res/values-uk/strings.xml | 2 -- app/src/main/res/values-vi/strings.xml | 2 -- app/src/main/res/values-zh-rCN/strings.xml | 2 -- app/src/main/res/values-zh-rHK/strings.xml | 2 -- app/src/main/res/values-zh-rMO/strings.xml | 1 - app/src/main/res/values-zh-rSG/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 2 -- 48 files changed, 1 insertion(+), 95 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index c3f1c8e08..5870dbc6a 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -10,7 +10,6 @@ تم رفض التصريح. فشل الحصول على رمز الولوج. إنّ المنشور طويل جدا! - يجب أن يكون حجم الملف أقل من 8 ميغابايت. يجب أن يكون حجم ملفات الفيديو أقل من 40 ميغا بايت. لا يمكن تحميل هذا النوع من الملفات. تعذر فتح ذاك الملف. @@ -483,7 +482,6 @@ القائمة ليس لديك أية مسودات. ليس لديك أية منشورات مُبرمَجة للنشر. - يجب أن يكون حجم الملفات الصوتية أقل مِن 40 ميغابايت. تُقدّر أدنى فترة لبرمجة النشر في ماستدون بـ 5 دقائق. تمكين حركات السحب للانتقال بين الألسنة طلب متابعة diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index a58d3eaf8..ed5002152 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -425,9 +425,7 @@ Изисква се разрешение за четене на носител. Този файл не можа да бъде отворен. Този тип файл не може да бъде качен. - Аудио файловете трябва да са по-малки от 40MB. Видео файловете трябва да са по-малки от 40MB. - Файлът трябва да е по-малък от 8MB. Състоянието е твърде дълго! Получаването на токен за вход бе неуспешно. Упълномощаването е отказано. diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 331e5e189..a58daecec 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -332,7 +332,6 @@ আলাপ বন্ধ করো আলাপ বন্ধ করো মাস্টোডনের সর্বনিম্ন ৫ মিনিটের সময়সূচীর বিরতি আছে। - অডিও ফাইলগুলি অবশ্যই ৪০MB এর চেয়ে কম হওয়া উচিত। তোমার কোনো খসড়া নেই। তোমার কোনো সময়সূচীত স্ট্যাটাস নেই। তালিকা @@ -418,7 +417,6 @@ মিডিয়া পড়তে অনুমতি প্রয়োজন। ওই ফাইল খোলা যাবে না। যে ধরনের ফাইল আপলোড করা যাবে না। - ভিডিও ফাইল 40MB চেয়ে কম হতে হবে। ফাইল 8MB চেয়ে কম হতে হবে। এই স্টেটাস টি খুব দীর্ঘ! একটি লগইন টোকেন পেতে ব্যর্থ। diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index 93f68dbd8..a19175c13 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -10,7 +10,6 @@ অনুমোদন অস্বীকার করা হয়েছে। একটি লগইন টোকেন পেতে ব্যর্থ। এই স্টেটাস টি খুব দীর্ঘ! - ফাইল 8MB চেয়ে কম হতে হবে। ভিডিও ফাইল 40MB চেয়ে কম হতে হবে। যে ধরনের ফাইল আপলোড করা যাবে না। ওই ফাইল খোলা যাবে না। @@ -421,7 +420,6 @@ বুকমার্ক %s আপনাকে অনুসরণ করার জন্য অনুরোধ করেছে বুকমার্কগুলি - অডিও ফাইলগুলি অবশ্যই ৪০MB এর চেয়ে কম হওয়া উচিত। অনুরোধ অনুসরণ করো বিজ্ঞপ্তি লুকাও নিঃশব্দ @%s\? diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index ddba8b510..47f72541d 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -9,7 +9,6 @@ S\'ha denegat l\'autorització. Ha fallat l\'obtenció del token d\'inici de sessió. L\'estat és massa llarg! - El fitxer ha de ser d\'una mida menor de 8MB. No es pot pujar aquest tipus de fitxer. No es pot obrir aquest tipus de fitxer. Cal permís d\'accés a l\'emmagatzematge. @@ -205,7 +204,6 @@ No hi ha res aquí. Elimina l\'impuls S\'ha produït un error de connexió! Comproveu la connexió i torneu-ho a provar! - Els fitxers de vídeo han de ser de mida menor de 40 MB. Multimèdia amagada Amaga Estàs segur de tancar la sessió de %1$s\? @@ -430,7 +428,6 @@ Llista S\'ha produït un error en cercar la publicació %s No tens cap estat planificat. - Els fitxers d\'àudio han de ser de mida menor de 40MB. No teniu cap esborrany. L\'interval mínim de planificació a Mastodon és de 5 minuts. Peticions de seguiment diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index 985ec63a5..81c5321fb 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -141,9 +141,7 @@ مۆڵەت بۆ خوێندنەوەی میدیا پێویستە. ئەم فایلە ناتوانرێت بکرێتەوە. ناتوانیت لەم جۆرە فایلانە بەرز بکەیتەوە. - دەبێت فایلە دەنگییەکان لە 40 مێگابایت گەورەتر نەبن. دەبێت ڤیدیۆکان لە 40 مێگابایت گەورەتر نەبن. - فایلەکە دەبێت لە 8 مێگابایت بچووکتر بێت. ئەم نووسینە زۆر درێژە! سەرکەوتوو نەبوو لە بەدەستهێنانی نیشانەی چوونەژوورەوە. ڕێپێدان ڕەتکرایەوە. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index aced0945d..e711c0007 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -10,7 +10,6 @@ Autorizace byla zamítnuta. Nepodařilo se získat přihlašovací token. Toot je příliš dlouhý! - Soubor musí být menší než 8 MB. Videosoubory musejí být menší než 40 MB. Tento typ souboru nemůže být nahrán. Tento soubor nemohl být otevřen. @@ -459,7 +458,6 @@ Záložky Záložka Záložky - Audio soubory musí být menší než 40MB. 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. diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 7d68e1e09..981f0ea38 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -9,7 +9,6 @@ Gwrthodwyd awdurdodi. Methu cael tocyn mewngofnodi. Mae\'r statws yn rhy hir! - Rhaid i\'r ffeil fod yn llai nag 8MB. Rhaid i ffeiliau fideo fod yn llai na 40MB. Ni allwch uwchlwytho\'r math hwnnw o ffeil. Nid oedd modd agor y ffeil honno. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 7c181ae4e..a4b4c7b59 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -10,7 +10,6 @@ Autorisierung fehlgeschlagen. Es konnte kein Login-Token abgerufen werden. Der Beitrag ist zu lang! - Die Datei muss kleiner als 8 MB sein. Videodateien müssen kleiner als 40 MB sein. Dieser Dateityp darf nicht hochgeladen werden. Die Datei konnte nicht geöffnet werden. @@ -425,7 +424,6 @@ Geplante Beiträge Plane Beitrag Zurücksetzen - Audiodateien müssen kleiner als 40 MB sein. Lesezeichen Lesezeichen Lesezeichen diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 85583a2b2..cef008748 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -49,7 +49,6 @@ Ακολουθήστε Αναφορά Σίγαση - Τα μουσικά αρχεία πρέπει να είναι μικρότερα από 40MB. Αφαίρεση αγαπημένου Αναφορά του/της %s Προτιμήσεις Λογαριασμού @@ -67,7 +66,6 @@ Αποθήκευση Γρήγορη Απάντηση Χρήστες σε σίγαση - Το αρχείο πρέπει να είναι μικρότερο από 8MB. Απόκρυψη προωθήσεων Προτιμήσεις Σύνδεση diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index de167b858..434160e76 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -42,14 +42,11 @@ An error occurred. Local Blocked users - Video files must be less than 40MB. Tabs - Audio files must be less than 40MB. Follow Requests Home Muted users Thread - The file must be less than 8MB. Error sending post. This cannot be empty. Permission to store media is required. diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 3f20878e9..f727cb8e8 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -10,7 +10,6 @@ Rajtigo rifuzita. Akiro de atingoĵetono malsukcesis. Via mesaĝo estas tro longa! - La dosiero devas esti malpli ol 8MB. Videaj dosieroj devas esti malpli ol 40MB. Tia dosiero ne estas rajtigita. Tiu dosiero ne povas esti malfermita. @@ -437,7 +436,6 @@ Elekti la liston Listo Eraro dum elserĉo de la mesaĝo %s - Aŭdia dosiero devas esti malpli ol 40MB. Vi ne havas iun ajn malneton. Vi ne havas iun ajn planitan mesaĝon. Petoj de sekvado diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 1f90c79e3..5ab2023b5 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -10,7 +10,6 @@ La autorización falló. Fallo al obtener identificador de login. ¡El estado es demasiado largo! - El archivo debe ser inferior a 8MB. Los archivos de vídeo deben tener menos de 40MB. No se admite este tipo de archivo. No pudo abrirse el fichero. @@ -450,7 +449,6 @@ Marcado como favorito Seleccionar lista Lista - Los ficheros de audio deben ser menores de 40MB. No tienes ningún borrador. No tienes ningún estado programado. Mastodon tiene un intervalo de programación mínimo de 5 minutos. diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index e8e1605ac..783c5119c 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -9,7 +9,6 @@ Akatsa baimentzerakoan. Akatsa login identifikatzailea lortzerakoan. Tut luzeegia! - Fitxategiak 8MB baino gutxiago izan behar ditu. Bideoak 40MB baino gutxiago izan behar ditu. Ez da fitxategi mota hau onartzen. Ezin izan da fitxategi hau ireki. @@ -441,7 +440,6 @@ Ireki bultzadaren egilea Denbora lerro publikoak Laster-markatuta - Audioak 40MB baino gutxiago izan behar ditu. Aukeratu zerrenda Zerrenda Ez duzu zirriborrorik. diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 4492e3ea4..a19fb16d5 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -9,7 +9,6 @@ احراز هویت رد شد. دریافت ژتون ورود شکست خورد. فرسته خیلی طولانی است! - پرونده باید کمتر از ۸ مگابایت باشد. پرونده ویدئویی باید کمتر از ۴۰ مگابایت باشد. این گونهٔ پرونده نمی‌تواند بارگذاری شود. این پرونده نتوانست گشوده شود. @@ -429,7 +428,6 @@ این حساب از کارسازی دیگر است. رونوشتی ناشناس از گزارش، به آن‌جا نیز ارسال شود؟ خطا در یافتن فرستهٔ %s قدرت‌گرفته از تاسکی - پرونده‌های صوتی باید کم‌تر از ۴۰م‌ب باشند. نشانک‌ها نشانک نشانک‌ها diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index d712bc215..6f92bb0fa 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -10,7 +10,6 @@ Authentification refusée. Impossible de récupérer le jeton d’authentification. Votre message est trop long ! - Le fichier doit avoir moins de 8 Mo. Les fichiers vidéos doivent avoir moins de 40 Mo. Ce type de fichier ne peut pas être téléversé. Le fichier ne peut pas être ouvert. @@ -451,7 +450,6 @@ Ajouté aux signets Sélectionner la liste Liste - Les fichiers audio doivent avoir moins de 40 Mo. Vous n’avez aucun brouillon. Vous n’avez aucun message planifié. L’intervalle minimum de planification sur Mastodon est de 5 minutes. diff --git a/app/src/main/res/values-fy/strings.xml b/app/src/main/res/values-fy/strings.xml index f902d7426..b0c5321b0 100644 --- a/app/src/main/res/values-fy/strings.xml +++ b/app/src/main/res/values-fy/strings.xml @@ -242,9 +242,7 @@ Tastimming om media te lêzen is nedich. Die triem koe net iepene wurde. Dat type triem kin net upload wurde. - Lûdstriemen moatte lytser as 40MB wêze. Fideo\'s moatte lytse as 40MB wêze. - De triem moat lytser as 8MB wêze. De status is te lang! Koe gjin ynlogtoken krije. Ferifikaasje ôfkard. diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index 382566c0a..868dc3253 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -156,9 +156,7 @@ Leabharmharc Ainmnigh Ní féidir an cineál comhaid sin a uaslódáil. - Caithfidh comhaid fuaime a bheith níos lú ná 40MB. Caithfidh comhaid físe a bheith níos lú ná 40MB. - Caithfidh an comhad a bheith níos lú ná 8MB. Tá an stádas ró-fhada! Theip ar chomhartha logála isteach a fháil. Diúltaíodh údarú. diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 294bdc56a..9bec6a8b6 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -520,9 +520,7 @@ Tha feum air cead gus meadhanan a leughadh. Cha b’ urrainn dhuinn am faidhle sin fhosgladh. Cha ghabh an seòrsa de dh’fhaidhle seo a luchdadh suas. - Feumaidh faidhlichean fuaime a bhith nas lugha na 40MB. Feumaidh faidhlichean video a bhith nas lugha na 40MB. - Feumaidh am faidhle a bhith nas lugha na 8MB. Tha am post ro fhada! Cha deach leinn tòcan clàraidh a-steach fhaighinn. Chaidh an t-ùghdarrachadh a dhiùltadh. diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 71273a3a4..5cc0f5a9d 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -112,9 +112,7 @@ Requírese o permiso de lectura do multimedia. Non se puido abrir o ficheiro. Non pode subirse ese tipo de ficheiro. - Os ficheiros de audio teñen que ser menores de 40MB. Os ficheiros de vídeo teñen que ser menores de 40MB. - O ficheiro debe ser menor de 8MB. A publicación é demasiado longa! Fallou a obtención do token de acceso. A autorización foi rexeitada. diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index dba9309b0..c9a760d5d 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -28,16 +28,13 @@ बंद करें प्रोफाइल अनुगामी - फ़ाइल 8 एमबी से कम होनी चाहिए। अपलोड विफल रहा। मीडिया को स्टोर करने की अनुमति आवश्यक है। मीडिया पढ़ने की अनुमति आवश्यक है। वह फ़ाइल नहीं खोली जा सकी। उस प्रकार की फ़ाइल अपलोड नहीं की जा सकती। - ऑडियो फाइलें 40 एमबी से कम होनी चाहिए। एक त्रुटि हुई। रीसेट - वीडियो फ़ाइलें 40MB से कम होनी चाहिए। लॉगिन टोकन प्राप्त करने में विफल। प्राधिकरण करने के से इनकार कर दिया। एक अज्ञात प्राधिकरण त्रुटि हुई। diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index a097e81e6..18c4765bb 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -10,7 +10,6 @@ Engedély megtagadva. Bejelentkezési token megszerzése sikertelen. Túl hosszú a bejegyzés! - A fájlnak kisebbnek kell lennie, mint 8 MB. A videofájloknak kisebbnek kell lenniük, mint 40 MB. Ilyen típusú fájlt nem lehet feltölteni. Fájl megnyitása sikertelen. @@ -446,7 +445,6 @@ Könyvjelzőzve Lista kiválasztása Lista - A hangfájloknak kisebbnek kell lenniük, mint 40 MB. Nincs egy piszkozatod sem. Nincs egy ütemezett bejegyzésed sem. A Mastodonban a legrövidebb ütemezhető időintervallum 5 perc. diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 77350a872..3f873a613 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -22,7 +22,6 @@ Heimild var hafnað. Mistókst að fá innskráningarteikn. Færslan er of löng! - Skráin verður að vera minni en 8MB. Myndskeiðaskrár verða að vera minni en 40MB. Þessa tegund skrár er ekki hægt að senda inn. Ekki var hægt að opna skrána. @@ -416,7 +415,6 @@ Villa við að fletta upp færslunni %s Þú ert ekki með nein drög. Þú ert ekki með neinar áætlaðar stöðufærslur. - Hljóðskrár verða að vera minni en 40MB. Mastodon er með 5 mínútna lágmarksbil fyrir áætlaðar aðgerðir. Fylgjendabeiðnir Myllumerki diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index c6af80f89..92b4faad8 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -10,7 +10,6 @@ Autorizzazione negata. Acquisizione token di accesso fallita. Il post è troppo lungo! - Il file deve essere più piccolo di 8 MB. I video devono essere più piccoli di 40 MB. Quel tipo di file non può essere caricato. Non è stato possibile aprire quel file. @@ -464,7 +463,6 @@ Smetti di silenziare la conversazione Silenzia conversazione %s ha chiesto di seguirti - I file audio devono essere più piccoli di 40 MB. Smetti di silenziare %s Richieste di seguirti Salvato! diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f5fcb6fe9..4487c6520 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -9,7 +9,6 @@ 承認が拒否されました。 ログイントークンの取得に失敗しました。 投稿文が長すぎます! - ファイルは4MB未満にしてください。 ビデオファイルは40MB未満にしてください。 その形式のファイルはアップロードできません。 ファイルを開けませんでした。 @@ -394,7 +393,6 @@ 検索に失敗しました 通知フィルターを表示 リセット - 音声ファイルは40MB未満にしてください。 ブックマーク ブックマーク 編集 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index e8143bb95..c50697f0d 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -10,7 +10,6 @@ 인증이 거부되었습니다. 로그인 토큰을 받아올 수 없습니다. 게시물 길이가 너무 깁니다! - 파일 크기가 8MB 이상인 사진은 업로드할 수 없습니다. 파일 크기가 40MB 이상인 동영상은 업로드할 수 없습니다. 이 파일은 첨부할 수 없습니다. 이 파일을 읽지 못했습니다. diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 951ed4757..75de222ae 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -22,7 +22,6 @@ ആധികാരികത ഉറപ്പുവരുത്താനായില്ല. ഒരു പ്രവേശന ടോക്കൺ ലഭ്യമാക്കുന്നതിൽ പരാജയപ്പെട്ടു. ഈ സ്റ്റാറ്റസ് വളരെ നീളമേറിയതാണ്! - ഫയൽ 8 എംബിയേക്കാളും ചെറുതായിരിക്കണം. ചലച്ചിത്ര ഫയലുകൾ 40എംബിയിലും ചെറുതായിരിക്കണം. ഇത്തരം ഫയൽ അപ്‌ലോഡ് ചെയ്യാൻ സാധിക്കില്ല. ഈ ഫയൽ തുറക്കാനായില്ല. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 7339b238d..186642500 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -10,7 +10,6 @@ Autorisatie werd geweigerd. Kon geen inlogsleutel verkrijgen. Tekst van deze toot is te lang! - Bestand moet kleiner zijn dan 8MB. Videobestanden moeten kleiner zijn dan 40MB. Bestandstype kan niet worden geüpload. Bestand kon niet worden geopend. @@ -435,7 +434,6 @@ Meerdere keuzes Keuze %d Bewerken - Geluidsbestanden moeten minder dan 40MB zijn. Bladwijzers Ingeplande toots Bladwijzer diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index 2fbaa7557..3893506ac 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -10,7 +10,6 @@ Autorisasjon ble nektet. Henting av logintoken feilet. Innlegget er for langt! - Filen må være mindre enn 8MB. Videofiler må være mindre enn 40MB. Den filtypen kan ikke lastes opp. Den filen kunne ikke åpnes. @@ -440,7 +439,6 @@ Liste Du har ingen planlagte innlegg. Du har ikke lagret noen kladder. - Lydfiler må være mindre enn 40MB. Mastodon har et minimums planleggingsinterval på 5 minutter. Vis forhåndsvisning av linker i tidslinjer Vis bekreftelsesdialog før boosting diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index c2115ac1d..0e2e270b1 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -9,7 +9,6 @@ L\'autoritzacion es estada regetada. Fracàs de l’obtencion del testimoni d\'iniciacion de session. L\'estatut es tròp long ! - Lo fichièr a d’èsser inferior a 8Mo. Los fichièrs vidèo devon pas far mai de 40 Mo. Aqueste tip de fichièr se pòt pas mandar. Aqueste tip de fichièr se pòt pas dobrir. @@ -449,7 +448,6 @@ Ajustat als marcapaginas Seleccionar la list Lista - Los fichièrs àudio devon èsser inferiors a 40 Mo. Avètz pas cap de borrolhon. Avètz pas cap de tut planificat. L’interval minimum de planificacion sus Mastodon e de 5 minutas. diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 2cf8bfad3..a3202e54b 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -9,7 +9,6 @@ Odmówiono autoryzacji. Nie udało się uzyskać tokenu logowania. Zbyt długi wpis! - Plik może mieć maksymalnie 8 MB. Ten format pliku nie może zostać wysłany. Nie można otworzyć tego pliku. Wymagane jest pozwolenie na dostęp do plików z urządzenia. @@ -267,7 +266,6 @@ Nazwa Zawartość Wystąpił problem z łącznością! Sprawdź swoje połączenie internetowe i spróbuj ponownie! - Pliki wideo muszą być mniejsze niż 40MB. Wiadomości bezpośrednie Przypięte Rozwiń @@ -464,7 +462,6 @@ Dodany do zakładek Wybierz listę Lista - Pliki audio muszą być mniejsze niż 40MB. Nie masz żadnych szkiców. Nie masz żadnych zaplanowanych wpisów. Mastodon umożliwia wysłanie minimalnie 5 minut od zaplanowania. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 2e53d6c13..2892a6a21 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -9,7 +9,6 @@ Autorização negada. Erro ao adquirir token de entrada. O toot é muito longo! - A imagem deve ser menor que 8MB. O vídeo deve ser menor que 40MB. Esse tipo de arquivo não pode ser enviado. Esse arquivo não pode ser aberto. @@ -446,7 +445,6 @@ Selecionar lista Lista Sem toots agendados. - O áudio deve ser menor que 40MB. Sem rascunhos. Mastodon possui um intervalo mínimo de 5 minutos para agendar. Seguidores pendentes diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 87dd3ca59..e10e6c5f3 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -24,9 +24,7 @@ Autorização negada. Erro ao adquirir token de login. O toot é muito extenso! - O ficheiro deve ter menor de 8MB. Os ficheiros de vídeo devem ter menor de 40MB. - Os ficheiros de áudio devem ter menor de 40MB. Esse tipo de ficheiro não pode ser enviado. Não foi possível abrir esse ficheiro. É necessária permissão para ler o armazenamento. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 98cf06234..f43576fd6 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -10,7 +10,6 @@ Авторизация была отклонена. Не удалось получить токен авторизации. Статус слишком длинный! - Файл должен быть не больше 8 Мбайт. Видео должно быть не больше 40 Мбайт. Данный тип файла не может быть загружен. Файл не может быть открыт. @@ -466,7 +465,6 @@ Добавлено в закладки Выбрать список Список - Аудиофайлы должны быть меньше 40МБ. Ошибка поиска поста %s У вас нет черновиков. У вас нет запланированный статусов. diff --git a/app/src/main/res/values-sa/strings.xml b/app/src/main/res/values-sa/strings.xml index e304b1657..48f16b627 100644 --- a/app/src/main/res/values-sa/strings.xml +++ b/app/src/main/res/values-sa/strings.xml @@ -11,9 +11,7 @@ श्रव्यदृश्यसामग्र्यः द्रष्टुमनुमतिर्दातव्या । सा सञ्चिका नोद्घाट्यते । नैतादृशा सञ्चिका उपारोपणीया । - श्रव्यसञ्चिका ४०MBतोऽल्पा स्थाप्या । चलचित्रसञ्चिका ४०MBतोऽल्पा स्थाप्या । - ८ MBतोऽल्पा परिमिता सञ्चिका स्थाप्या । सम्प्रवेशस्तोकं न लब्धः । प्रमाणीकरणं निषिद्धम् । अज्ञातः प्रमाणीकरणदोषो जातः । diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index e06555c71..22a5d9872 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -34,9 +34,7 @@ Toto nemôže byť prázdne. Autorizácia bola zamietnutá. Nepodarilo sa získať prihlasovací token. - Súbor musí byť menší ako 8 MB. Videosúbory musia byť menšie ako 40 MB. - Audio súbory musia byť menšie ako 40 MB. Súbor sa nepodarilo otvoriť. Domov Panely diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index caa491019..ff80e0332 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -8,7 +8,6 @@ Pooblastitev je bila zavrnjena. Ni bilo mogoče pridobiti žetona za prijavo. Status je predolgo! - Datoteka mora biti manjša od 8 MB. Video datoteke morajo biti manjše od 40 MB. Te vrste datoteke ni mogoče poslati. Te datoteke ni bilo mogoče odpreti. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 5e8a011aa..f1f90a78a 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -10,7 +10,6 @@ Ingen behörighet. Misslyckades med att få en inloggnings-token. Statusen är för lång! - Filen måste vara mindre än 8MB. Videofiler måste vara mindre än 40MB. Den typen av fil kan inte laddas upp. Den filen kunde inte öppnas. @@ -454,7 +453,6 @@ Välj lista Lista Du har inga schemalagda statusar. - Ljudfiler måste vara mindre än 40MB. Du har inga utkast. Mastodon har ett minimalt schemaläggningsintervall på 5 minuter. Tysta konversation diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 7604eca58..dd9c92089 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -9,7 +9,6 @@ அங்கீகாரம் மறுக்கப்பட்டுள்ளது உள்நுழைவு டோக்கனைப் பெறுவதில் தோல்வி. நிலை மிக நீளமாக உள்ளது! - கோப்பு 4MB-க்கும் குறைவாக இருக்க வேண்டும். இந்த வகை கோப்பை பதிவேற்ற முடியாது. அந்த கோப்பை திறக்க முடியவில்லை. ஊடகத்தை படிக்க அனுமதி தேவை. @@ -260,7 +259,6 @@ பொருத்து கணக்கரின் முன்னுரிமைகள் பிணைய பிழை ஏற்பட்டது! உங்கள் இணைப்பைச் சரிபார்த்து மீண்டும் முயற்சிக்கவும்! - காணொளி 40MB க்கும் குறைவாக இருக்க வேண்டும். டூத் அனுப்புவதில் பிழை ஏற்பட்டுள்ளது நேரடி தகவல் பட்டைகள் diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index e094542c9..43d671fdd 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -80,7 +80,7 @@ ชื่นชอบโดย บูสต์โดย - <b>%s</b> บูสต์ + <b>%s</b> บูสต์ <b>%1$s</b> ชื่นชอบ @@ -414,9 +414,7 @@ ต้องมีสิทธิ์อ่านสื่อ ไม่สามารถเปิดไฟล์ได้ ไม่สามารถอัปโหลดไฟล์ประเภทนี้ได้ - ไฟล์เสียงต้องมีขนาดน้อยกว่า 40MB ไฟล์วิดีโอต้องมีขนาดน้อยกว่า 40MB - ไฟล์ต้องมีขนาดน้อยกว่า 8MB ข้อความสถานะยาวเกินไป! ไม่สามารถรับโทเค็นการเข้าสู่ระบบ การขออนุญาตสิทธิถูกปฏิเสธ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e4bb815f3..37c3f17eb 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -10,7 +10,6 @@ Yetkilendirme reddedildi. Giriş belirteci alınırken hata oluştu. Durum çok uzun! - Dosya 8 MB\'dan küçük olmalı. Video dosyaları 40 MB’dan küçük olmalı. Bu tür bir dosya yüklenemez. Dosya açılamadı. @@ -465,7 +464,6 @@ %s kullanıcısından gelen bildirimleri göster %s sesini aç %s seni takip etmek istiyor - Ses dosyaları 40 MB\'dan küçük olmalı. Sohbetin sesini aç takip istendi Sohbeti sessize al diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 1a9709441..f8dcb4af8 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -21,9 +21,7 @@ Потрібен дозвіл на читання медіа. Не вдається відкрити цей файл. Неможливо відвантажити файл цього типу. - Аудіофайли повинні бути менше 40 МБ. Відео повинне бути менше 40 МБ. - Файл повинен бути менше 8 МБ. Допис задовгий! Не вдалося знайти браузер, який можна використати. Не може бути порожнім. diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 5b117fa8a..627863a66 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -31,9 +31,7 @@ Cần có quyền đọc tập tin. Không thể mở tập tin. Không hỗ trợ định dạng này. - Kích cỡ audio tối đa 40MB. Kích cỡ video tối đa 40MB. - Kích cỡ hình ảnh tối đa là 8MB. Vượt quá số ký tự cho phép! Lấy token đăng nhập thất bại. Truy cập bị từ chối. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 99a329c41..04e25e8d8 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -10,7 +10,6 @@ 授权被拒绝。 未能获取登录令牌。 嘟文太长了! - 文件大小限制为 8MB。 视频文件大小限制为 40MB。 无法上传此类型的文件。 打不开此文件。 @@ -397,7 +396,6 @@ 剩余 %d 秒 重置 - 音频文件大小限制为 40M。 书签 隐藏的域名 定时嘟文 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index 5d3779f23..eca24b9e7 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -10,7 +10,6 @@ 授權被拒絕。 無法獲取登入資訊。 嘟文太長了! - 檔案大小限制 8MB。 影片大小限制 40MB。 無法上傳此類型的檔案。 此檔案無法開啟。 @@ -444,7 +443,6 @@ 公告 已排程的嘟文 被隱藏的網域 - 聲音檔大小限制 40MB。 完整字詞 你的草稿欲回覆的原嘟文已被刪除 草稿已刪除 diff --git a/app/src/main/res/values-zh-rMO/strings.xml b/app/src/main/res/values-zh-rMO/strings.xml index 20c258a5e..5fbceafe7 100644 --- a/app/src/main/res/values-zh-rMO/strings.xml +++ b/app/src/main/res/values-zh-rMO/strings.xml @@ -10,7 +10,6 @@ 授權被拒絕 無法獲取登入資訊 嘟文太長了! - 檔案大小限制 8MB 影片大小限制 40MB 無法上傳此類型的檔案 此檔案無法開啟 diff --git a/app/src/main/res/values-zh-rSG/strings.xml b/app/src/main/res/values-zh-rSG/strings.xml index 4dfdaf49e..76b858df1 100644 --- a/app/src/main/res/values-zh-rSG/strings.xml +++ b/app/src/main/res/values-zh-rSG/strings.xml @@ -10,7 +10,6 @@ 授权被拒绝 无法获取登录信息 嘟文太长了! - 文件大小限制 8MB 视频文件大小限制 40MB 无法上传此类型的文件 此文件无法打开 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 1f9053c70..30e5765dc 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -10,7 +10,6 @@ 授權被拒絕。 無法獲取登入資訊。 嘟文太長了! - 檔案大小限制 8MB。 影片大小限制 40MB。 無法上傳此類型的檔案。 此檔案無法開啟。 @@ -435,7 +434,6 @@ 編輯 編輯 書籤 - 音檔必需小於40MB。 隱藏個人頁面中的狀態數量資訊 隱藏貼文上的狀態數量資訊 限制時間軸通知 From 2a2d96fbd9291f1d0d2a08e6fb0fde4990482f1a Mon Sep 17 00:00:00 2001 From: Weblate Date: Sat, 6 Aug 2022 06:42:33 +0000 Subject: [PATCH 016/142] Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Weblate Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ Translation: Tusky/Tusky --- app/src/main/res/values-ar/strings.xml | 1 - app/src/main/res/values-bg/strings.xml | 1 - app/src/main/res/values-bn-rBD/strings.xml | 1 - app/src/main/res/values-bn-rIN/strings.xml | 1 - app/src/main/res/values-ckb/strings.xml | 1 - app/src/main/res/values-cs/strings.xml | 1 - app/src/main/res/values-cy/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-eo/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-eu/strings.xml | 1 - app/src/main/res/values-fa/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-fy/strings.xml | 1 - app/src/main/res/values-ga/strings.xml | 1 - app/src/main/res/values-gd/strings.xml | 1 - app/src/main/res/values-gl/strings.xml | 1 - app/src/main/res/values-hu/strings.xml | 1 - app/src/main/res/values-is/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-ja/strings.xml | 1 - app/src/main/res/values-ko/strings.xml | 1 - app/src/main/res/values-ml/strings.xml | 1 - app/src/main/res/values-nl/strings.xml | 1 - app/src/main/res/values-no-rNB/strings.xml | 1 - app/src/main/res/values-oc/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-pt-rPT/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-sa/strings.xml | 1 - app/src/main/res/values-sk/strings.xml | 1 - app/src/main/res/values-sl/strings.xml | 1 - app/src/main/res/values-sv/strings.xml | 1 - app/src/main/res/values-th/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-uk/strings.xml | 1 - app/src/main/res/values-vi/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values-zh-rHK/strings.xml | 1 - app/src/main/res/values-zh-rMO/strings.xml | 1 - app/src/main/res/values-zh-rSG/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 1 - 42 files changed, 42 deletions(-) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 5870dbc6a..55e637d53 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -10,7 +10,6 @@ تم رفض التصريح. فشل الحصول على رمز الولوج. إنّ المنشور طويل جدا! - يجب أن يكون حجم ملفات الفيديو أقل من 40 ميغا بايت. لا يمكن تحميل هذا النوع من الملفات. تعذر فتح ذاك الملف. التصريح لازم لقراءة الوسائط. diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index ed5002152..05d91ba8d 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -425,7 +425,6 @@ Изисква се разрешение за четене на носител. Този файл не можа да бъде отворен. Този тип файл не може да бъде качен. - Видео файловете трябва да са по-малки от 40MB. Състоянието е твърде дълго! Получаването на токен за вход бе неуспешно. Упълномощаването е отказано. diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index a58daecec..f719128b5 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -417,7 +417,6 @@ মিডিয়া পড়তে অনুমতি প্রয়োজন। ওই ফাইল খোলা যাবে না। যে ধরনের ফাইল আপলোড করা যাবে না। - ফাইল 8MB চেয়ে কম হতে হবে। এই স্টেটাস টি খুব দীর্ঘ! একটি লগইন টোকেন পেতে ব্যর্থ। অনুমোদন অস্বীকার করা হয়েছে। diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index a19175c13..eac49ae51 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -10,7 +10,6 @@ অনুমোদন অস্বীকার করা হয়েছে। একটি লগইন টোকেন পেতে ব্যর্থ। এই স্টেটাস টি খুব দীর্ঘ! - ভিডিও ফাইল 40MB চেয়ে কম হতে হবে। যে ধরনের ফাইল আপলোড করা যাবে না। ওই ফাইল খোলা যাবে না। মিডিয়া পড়তে অনুমতি প্রয়োজন। diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index 81c5321fb..13b1a2355 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -141,7 +141,6 @@ مۆڵەت بۆ خوێندنەوەی میدیا پێویستە. ئەم فایلە ناتوانرێت بکرێتەوە. ناتوانیت لەم جۆرە فایلانە بەرز بکەیتەوە. - دەبێت ڤیدیۆکان لە 40 مێگابایت گەورەتر نەبن. ئەم نووسینە زۆر درێژە! سەرکەوتوو نەبوو لە بەدەستهێنانی نیشانەی چوونەژوورەوە. ڕێپێدان ڕەتکرایەوە. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index e711c0007..46c95f8ad 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -10,7 +10,6 @@ Autorizace byla zamítnuta. Nepodařilo se získat přihlašovací token. Toot je příliš dlouhý! - Videosoubory musejí být menší než 40 MB. Tento typ souboru nemůže být nahrán. Tento soubor nemohl být otevřen. Je vyžadováno povolení číst média. diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 981f0ea38..76ea1e06d 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -9,7 +9,6 @@ Gwrthodwyd awdurdodi. Methu cael tocyn mewngofnodi. Mae\'r statws yn rhy hir! - Rhaid i ffeiliau fideo fod yn llai na 40MB. Ni allwch uwchlwytho\'r math hwnnw o ffeil. Nid oedd modd agor y ffeil honno. Rhaid cael caniatâd i ddarllen hwn. diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index a4b4c7b59..369a07d14 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -10,7 +10,6 @@ Autorisierung fehlgeschlagen. Es konnte kein Login-Token abgerufen werden. Der Beitrag ist zu lang! - Videodateien müssen kleiner als 40 MB sein. Dieser Dateityp darf nicht hochgeladen werden. Die Datei konnte nicht geöffnet werden. Eine Leseberechtigung wird für das Hochladen der Mediendatei benötigt. diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index f727cb8e8..2db0ebb27 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -10,7 +10,6 @@ Rajtigo rifuzita. Akiro de atingoĵetono malsukcesis. Via mesaĝo estas tro longa! - Videaj dosieroj devas esti malpli ol 40MB. Tia dosiero ne estas rajtigita. Tiu dosiero ne povas esti malfermita. Permeso legi aŭdovidaĵojn necesas. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5ab2023b5..592c212a0 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -10,7 +10,6 @@ La autorización falló. Fallo al obtener identificador de login. ¡El estado es demasiado largo! - Los archivos de vídeo deben tener menos de 40MB. No se admite este tipo de archivo. No pudo abrirse el fichero. Se requiere permiso para acceder al almacenamiento. diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 783c5119c..03344ed20 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -9,7 +9,6 @@ Akatsa baimentzerakoan. Akatsa login identifikatzailea lortzerakoan. Tut luzeegia! - Bideoak 40MB baino gutxiago izan behar ditu. Ez da fitxategi mota hau onartzen. Ezin izan da fitxategi hau ireki. Memoriara sartzeko baimena behar da. diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index a19fb16d5..66f5826cb 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -9,7 +9,6 @@ احراز هویت رد شد. دریافت ژتون ورود شکست خورد. فرسته خیلی طولانی است! - پرونده ویدئویی باید کمتر از ۴۰ مگابایت باشد. این گونهٔ پرونده نمی‌تواند بارگذاری شود. این پرونده نتوانست گشوده شود. نیاز به اجازهٔ خواندن رسانه است. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 6f92bb0fa..8ff97adbd 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -10,7 +10,6 @@ Authentification refusée. Impossible de récupérer le jeton d’authentification. Votre message est trop long ! - Les fichiers vidéos doivent avoir moins de 40 Mo. Ce type de fichier ne peut pas être téléversé. Le fichier ne peut pas être ouvert. Permission requise pour lire le média. diff --git a/app/src/main/res/values-fy/strings.xml b/app/src/main/res/values-fy/strings.xml index b0c5321b0..cce1246d0 100644 --- a/app/src/main/res/values-fy/strings.xml +++ b/app/src/main/res/values-fy/strings.xml @@ -242,7 +242,6 @@ Tastimming om media te lêzen is nedich. Die triem koe net iepene wurde. Dat type triem kin net upload wurde. - Fideo\'s moatte lytse as 40MB wêze. De status is te lang! Koe gjin ynlogtoken krije. Ferifikaasje ôfkard. diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index 868dc3253..19b67ebbc 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -156,7 +156,6 @@ Leabharmharc Ainmnigh Ní féidir an cineál comhaid sin a uaslódáil. - Caithfidh comhaid físe a bheith níos lú ná 40MB. Tá an stádas ró-fhada! Theip ar chomhartha logála isteach a fháil. Diúltaíodh údarú. diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 9bec6a8b6..9aa460765 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -520,7 +520,6 @@ Tha feum air cead gus meadhanan a leughadh. Cha b’ urrainn dhuinn am faidhle sin fhosgladh. Cha ghabh an seòrsa de dh’fhaidhle seo a luchdadh suas. - Feumaidh faidhlichean video a bhith nas lugha na 40MB. Tha am post ro fhada! Cha deach leinn tòcan clàraidh a-steach fhaighinn. Chaidh an t-ùghdarrachadh a dhiùltadh. diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 5cc0f5a9d..4c7d08b2d 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -112,7 +112,6 @@ Requírese o permiso de lectura do multimedia. Non se puido abrir o ficheiro. Non pode subirse ese tipo de ficheiro. - Os ficheiros de vídeo teñen que ser menores de 40MB. A publicación é demasiado longa! Fallou a obtención do token de acceso. A autorización foi rexeitada. diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 18c4765bb..9745eeb3f 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -10,7 +10,6 @@ Engedély megtagadva. Bejelentkezési token megszerzése sikertelen. Túl hosszú a bejegyzés! - A videofájloknak kisebbnek kell lenniük, mint 40 MB. Ilyen típusú fájlt nem lehet feltölteni. Fájl megnyitása sikertelen. Média olvasási engedély szükséges. diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 3f873a613..28244b188 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -22,7 +22,6 @@ Heimild var hafnað. Mistókst að fá innskráningarteikn. Færslan er of löng! - Myndskeiðaskrár verða að vera minni en 40MB. Þessa tegund skrár er ekki hægt að senda inn. Ekki var hægt að opna skrána. Krafist er heimilda til að lesa gögn. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 92b4faad8..54da752b5 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -10,7 +10,6 @@ Autorizzazione negata. Acquisizione token di accesso fallita. Il post è troppo lungo! - I video devono essere più piccoli di 40 MB. Quel tipo di file non può essere caricato. Non è stato possibile aprire quel file. È richiesto il permesso di leggere file. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 4487c6520..706318221 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -9,7 +9,6 @@ 承認が拒否されました。 ログイントークンの取得に失敗しました。 投稿文が長すぎます! - ビデオファイルは40MB未満にしてください。 その形式のファイルはアップロードできません。 ファイルを開けませんでした。 メディアの読み取り許可が必要です。 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index c50697f0d..5430382b3 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -10,7 +10,6 @@ 인증이 거부되었습니다. 로그인 토큰을 받아올 수 없습니다. 게시물 길이가 너무 깁니다! - 파일 크기가 40MB 이상인 동영상은 업로드할 수 없습니다. 이 파일은 첨부할 수 없습니다. 이 파일을 읽지 못했습니다. 미디어를 읽기 위한 권한이 필요합니다. diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 75de222ae..ea7902197 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -22,7 +22,6 @@ ആധികാരികത ഉറപ്പുവരുത്താനായില്ല. ഒരു പ്രവേശന ടോക്കൺ ലഭ്യമാക്കുന്നതിൽ പരാജയപ്പെട്ടു. ഈ സ്റ്റാറ്റസ് വളരെ നീളമേറിയതാണ്! - ചലച്ചിത്ര ഫയലുകൾ 40എംബിയിലും ചെറുതായിരിക്കണം. ഇത്തരം ഫയൽ അപ്‌ലോഡ് ചെയ്യാൻ സാധിക്കില്ല. ഈ ഫയൽ തുറക്കാനായില്ല. മീഡിയ വായിക്കുവാനുള്ള അനുമതി ആവശ്യമാണ്. diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 186642500..2716427fd 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -10,7 +10,6 @@ Autorisatie werd geweigerd. Kon geen inlogsleutel verkrijgen. Tekst van deze toot is te lang! - Videobestanden moeten kleiner zijn dan 40MB. Bestandstype kan niet worden geüpload. Bestand kon niet worden geopend. Er is toestemming nodig om deze media te lezen. diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index 3893506ac..54afe19d9 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -10,7 +10,6 @@ Autorisasjon ble nektet. Henting av logintoken feilet. Innlegget er for langt! - Videofiler må være mindre enn 40MB. Den filtypen kan ikke lastes opp. Den filen kunne ikke åpnes. Trenger tillatelse til å lese media. diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index 0e2e270b1..21a432ac1 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -9,7 +9,6 @@ L\'autoritzacion es estada regetada. Fracàs de l’obtencion del testimoni d\'iniciacion de session. L\'estatut es tròp long ! - Los fichièrs vidèo devon pas far mai de 40 Mo. Aqueste tip de fichièr se pòt pas mandar. Aqueste tip de fichièr se pòt pas dobrir. Cal permís de lectura del mèdia. diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 2892a6a21..3860c9ab5 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -9,7 +9,6 @@ Autorização negada. Erro ao adquirir token de entrada. O toot é muito longo! - O vídeo deve ser menor que 40MB. Esse tipo de arquivo não pode ser enviado. Esse arquivo não pode ser aberto. Permissão para ler mídia é necessária. diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index e10e6c5f3..92e9b13d7 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -24,7 +24,6 @@ Autorização negada. Erro ao adquirir token de login. O toot é muito extenso! - Os ficheiros de vídeo devem ter menor de 40MB. Esse tipo de ficheiro não pode ser enviado. Não foi possível abrir esse ficheiro. É necessária permissão para ler o armazenamento. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index f43576fd6..11b6cb2c2 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -10,7 +10,6 @@ Авторизация была отклонена. Не удалось получить токен авторизации. Статус слишком длинный! - Видео должно быть не больше 40 Мбайт. Данный тип файла не может быть загружен. Файл не может быть открыт. Необходимо разрешение на чтение медиаконтента. diff --git a/app/src/main/res/values-sa/strings.xml b/app/src/main/res/values-sa/strings.xml index 48f16b627..68fc3da82 100644 --- a/app/src/main/res/values-sa/strings.xml +++ b/app/src/main/res/values-sa/strings.xml @@ -11,7 +11,6 @@ श्रव्यदृश्यसामग्र्यः द्रष्टुमनुमतिर्दातव्या । सा सञ्चिका नोद्घाट्यते । नैतादृशा सञ्चिका उपारोपणीया । - चलचित्रसञ्चिका ४०MBतोऽल्पा स्थाप्या । सम्प्रवेशस्तोकं न लब्धः । प्रमाणीकरणं निषिद्धम् । अज्ञातः प्रमाणीकरणदोषो जातः । diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 22a5d9872..e15463628 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -34,7 +34,6 @@ Toto nemôže byť prázdne. Autorizácia bola zamietnutá. Nepodarilo sa získať prihlasovací token. - Videosúbory musia byť menšie ako 40 MB. Súbor sa nepodarilo otvoriť. Domov Panely diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index ff80e0332..a69bcb7a7 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -8,7 +8,6 @@ Pooblastitev je bila zavrnjena. Ni bilo mogoče pridobiti žetona za prijavo. Status je predolgo! - Video datoteke morajo biti manjše od 40 MB. Te vrste datoteke ni mogoče poslati. Te datoteke ni bilo mogoče odpreti. Potrebno je dovoljenje za branje medijev. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index f1f90a78a..cad2bdf69 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -10,7 +10,6 @@ Ingen behörighet. Misslyckades med att få en inloggnings-token. Statusen är för lång! - Videofiler måste vara mindre än 40MB. Den typen av fil kan inte laddas upp. Den filen kunde inte öppnas. Behörighet att läsa media krävs. diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 43d671fdd..ad258560b 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -414,7 +414,6 @@ ต้องมีสิทธิ์อ่านสื่อ ไม่สามารถเปิดไฟล์ได้ ไม่สามารถอัปโหลดไฟล์ประเภทนี้ได้ - ไฟล์วิดีโอต้องมีขนาดน้อยกว่า 40MB ข้อความสถานะยาวเกินไป! ไม่สามารถรับโทเค็นการเข้าสู่ระบบ การขออนุญาตสิทธิถูกปฏิเสธ diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 37c3f17eb..4083f9e36 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -10,7 +10,6 @@ Yetkilendirme reddedildi. Giriş belirteci alınırken hata oluştu. Durum çok uzun! - Video dosyaları 40 MB’dan küçük olmalı. Bu tür bir dosya yüklenemez. Dosya açılamadı. Medya okuma izni gerekli. diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index f8dcb4af8..7bddf3412 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -21,7 +21,6 @@ Потрібен дозвіл на читання медіа. Не вдається відкрити цей файл. Неможливо відвантажити файл цього типу. - Відео повинне бути менше 40 МБ. Допис задовгий! Не вдалося знайти браузер, який можна використати. Не може бути порожнім. diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 627863a66..28c803a48 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -31,7 +31,6 @@ Cần có quyền đọc tập tin. Không thể mở tập tin. Không hỗ trợ định dạng này. - Kích cỡ video tối đa 40MB. Vượt quá số ký tự cho phép! Lấy token đăng nhập thất bại. Truy cập bị từ chối. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 04e25e8d8..70dac4724 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -10,7 +10,6 @@ 授权被拒绝。 未能获取登录令牌。 嘟文太长了! - 视频文件大小限制为 40MB。 无法上传此类型的文件。 打不开此文件。 需要授予 Tusky 读取媒体文件的权限。 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index eca24b9e7..a7bf604d3 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -10,7 +10,6 @@ 授權被拒絕。 無法獲取登入資訊。 嘟文太長了! - 影片大小限制 40MB。 無法上傳此類型的檔案。 此檔案無法開啟。 需要授予 Tusky 讀取媒體檔案的權限。 diff --git a/app/src/main/res/values-zh-rMO/strings.xml b/app/src/main/res/values-zh-rMO/strings.xml index 5fbceafe7..4f778ce03 100644 --- a/app/src/main/res/values-zh-rMO/strings.xml +++ b/app/src/main/res/values-zh-rMO/strings.xml @@ -10,7 +10,6 @@ 授權被拒絕 無法獲取登入資訊 嘟文太長了! - 影片大小限制 40MB 無法上傳此類型的檔案 此檔案無法開啟 需要授予 Tusky 讀取媒體檔案的權限 diff --git a/app/src/main/res/values-zh-rSG/strings.xml b/app/src/main/res/values-zh-rSG/strings.xml index 76b858df1..a1aa0d7c5 100644 --- a/app/src/main/res/values-zh-rSG/strings.xml +++ b/app/src/main/res/values-zh-rSG/strings.xml @@ -10,7 +10,6 @@ 授权被拒绝 无法获取登录信息 嘟文太长了! - 视频文件大小限制 40MB 无法上传此类型的文件 此文件无法打开 需要授予 Tusky 读取媒体文件的权限 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 30e5765dc..c2c8ea679 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -10,7 +10,6 @@ 授權被拒絕。 無法獲取登入資訊。 嘟文太長了! - 影片大小限制 40MB。 無法上傳此類型的檔案。 此檔案無法開啟。 需要授予 Tusky 讀取媒體檔案的權限。 From 46e78210afa5766385a2a1d14fee21e20b0d82b8 Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Sat, 6 Aug 2022 06:42:33 +0000 Subject: [PATCH 017/142] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (487 of 487 strings) Co-authored-by: Vegard Skjefstad Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nb_NO/ Translation: Tusky/Tusky --- app/src/main/res/values-no-rNB/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index 54afe19d9..5ae17f236 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -536,4 +536,5 @@ Rediger bilde Bildet kunne ikke redigeres. Lasting av kontodetaljer feilet + Video- og lydfiler kan ikke være større enn %s MB. \ No newline at end of file From 5eb0764bd998761b99c74665b0630abf083d0efd Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 6 Aug 2022 06:42:33 +0000 Subject: [PATCH 018/142] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (487 of 487 strings) Co-authored-by: Eric Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/ Translation: Tusky/Tusky --- app/src/main/res/values-zh-rCN/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 70dac4724..6b5a10c15 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -544,4 +544,5 @@ 编辑图片 无法编辑图片。 加载账户详情失败 + 音视频文件大小不能超出 %s MB。 \ No newline at end of file From 3ee5efcb43df4a1ad7b119ff79668b5fb44bedad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Sat, 6 Aug 2022 06:42:33 +0000 Subject: [PATCH 019/142] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (487 of 487 strings) Co-authored-by: Hồ Nhất Duy Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ Translation: Tusky/Tusky --- app/src/main/res/values-vi/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 28c803a48..bf4c575be 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -525,4 +525,5 @@ Sửa ảnh Hình ảnh này không thể sửa. Không thể tải thông tin tài khoản + Video và audio không thể quá %s MB. \ No newline at end of file From 93d5cb1e0c1dd3c537f2ca0c6515afcada145b55 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 6 Aug 2022 09:00:55 +0200 Subject: [PATCH 020/142] update bitrise build status badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4b3ce9ef1..6a2840a0b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Translate - with Weblate](https://img.shields.io/badge/translate%20with-Weblate-green.svg?style=flat)](https://weblate.tusky.app/) [![OpenCollective](https://opencollective.com/tusky/backers/badge.svg)](https://opencollective.com/tusky/) [![Build Status](https://app.bitrise.io/app/a3e773c3c57a894c/status.svg?token=qLu_Ti4Gp2LWcYT4eo2INQ&branch=master)](https://app.bitrise.io/app/a3e773c3c57a894c#/builds) +[![Translate - with Weblate](https://img.shields.io/badge/translate%20with-Weblate-green.svg?style=flat)](https://weblate.tusky.app/) [![OpenCollective](https://opencollective.com/tusky/backers/badge.svg)](https://opencollective.com/tusky/) [![Build Status](https://app.bitrise.io/app/a3e773c3c57a894c/status.svg?token=qLu_Ti4Gp2LWcYT4eo2INQ&branch=develop)](https://app.bitrise.io/app/a3e773c3c57a894c) # Tusky ![](/fastlane/metadata/android/en-US/images/icon.png) From 042176e5230bfaec21dd4237b9c74c856c514c43 Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Sun, 7 Aug 2022 19:09:26 +0200 Subject: [PATCH 021/142] Add support for following hashtags (#2642) * Add support for following hashtags. Addresses #2637 * Update rxjava to coroutines * Update new tag api to use suspend functions * Update hashtag unfollow icon * Set correct tint on hashtag follow/unfollow icons * Translate hashtag follow/unfollow error messages * Toast => Snackbar * Remove unnecessary view lookup --- .../keylesspalace/tusky/StatusListActivity.kt | 84 ++++++++++++++++++- .../com/keylesspalace/tusky/entity/HashTag.kt | 2 +- .../tusky/network/MastodonApi.kt | 10 +++ .../res/drawable/ic_person_remove_24dp.xml | 8 ++ .../main/res/menu/view_hashtag_toolbar.xml | 21 +++++ app/src/main/res/values/strings.xml | 2 + 6 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 app/src/main/res/drawable/ic_person_remove_24dp.xml create mode 100644 app/src/main/res/menu/view_hashtag_toolbar.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt index 5ae1591c6..cc12479ad 100644 --- a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt @@ -18,12 +18,20 @@ package com.keylesspalace.tusky import android.content.Context import android.content.Intent import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem import androidx.fragment.app.commit +import androidx.lifecycle.lifecycleScope +import at.connyduck.calladapter.networkresult.fold +import com.google.android.material.snackbar.Snackbar 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.util.viewBinding import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector +import kotlinx.coroutines.launch import javax.inject.Inject class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { @@ -31,16 +39,21 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { @Inject lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector + 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 + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val binding = ActivityStatuslistBinding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.includedToolbar.toolbar) - val kind = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!) + kind = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!) val listId = intent.getStringExtra(EXTRA_LIST_ID) - val hashtag = intent.getStringExtra(EXTRA_HASHTAG) + hashtag = intent.getStringExtra(EXTRA_HASHTAG) val title = when (kind) { Kind.FAVOURITES -> getString(R.string.title_favourites) @@ -67,6 +80,70 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { } } + override fun onCreateOptionsMenu(menu: Menu): Boolean { + val tag = hashtag + if (kind == Kind.TAG && tag != null) { + lifecycleScope.launch { + mastodonApi.tag(tag).fold( + { tagEntity -> + menuInflater.inflate(R.menu.view_hashtag_toolbar, menu) + followTagItem = menu.findItem(R.id.action_follow_hashtag) + unfollowTagItem = menu.findItem(R.id.action_unfollow_hashtag) + followTagItem?.isVisible = tagEntity.following == false + unfollowTagItem?.isVisible = tagEntity.following == true + followTagItem?.setOnMenuItemClickListener { followTag() } + unfollowTagItem?.setOnMenuItemClickListener { unfollowTag() } + }, + { + Log.w(TAG, "Failed to query tag #$tag", it) + } + ) + } + } + + return super.onCreateOptionsMenu(menu) + } + + private fun followTag(): Boolean { + val tag = hashtag + if (tag != null) { + lifecycleScope.launch { + mastodonApi.followTag(tag).fold( + { + followTagItem?.isVisible = false + unfollowTagItem?.isVisible = true + }, + { + Snackbar.make(binding.root, getString(R.string.error_following_hashtag_format, tag), Snackbar.LENGTH_SHORT).show() + Log.e(TAG, "Failed to follow #$tag", it) + } + ) + } + } + + return true + } + + private fun unfollowTag(): Boolean { + val tag = hashtag + if (tag != null) { + lifecycleScope.launch { + mastodonApi.unfollowTag(tag).fold( + { + followTagItem?.isVisible = true + unfollowTagItem?.isVisible = false + }, + { + Snackbar.make(binding.root, getString(R.string.error_unfollowing_hashtag_format, tag), Snackbar.LENGTH_SHORT).show() + Log.e(TAG, "Failed to unfollow #$tag", it) + } + ) + } + } + + return true + } + override fun androidInjector() = dispatchingAndroidInjector companion object { @@ -75,6 +152,7 @@ class StatusListActivity : BottomSheetActivity(), HasAndroidInjector { private const val EXTRA_LIST_ID = "id" private const val EXTRA_LIST_TITLE = "title" private const val EXTRA_HASHTAG = "tag" + const val TAG = "StatusListActivity" fun newFavouritesIntent(context: Context) = Intent(context, StatusListActivity::class.java).apply { diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt b/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt index f3a4f65b8..e2401d939 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt @@ -1,3 +1,3 @@ package com.keylesspalace.tusky.entity -data class HashTag(val name: String, val url: String) +data class HashTag(val name: String, val url: String, val following: Boolean? = null) 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 e1d18e9f6..12d142ba5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -25,6 +25,7 @@ import com.keylesspalace.tusky.entity.Conversation import com.keylesspalace.tusky.entity.DeletedStatus import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.Marker import com.keylesspalace.tusky.entity.MastoList @@ -656,4 +657,13 @@ interface MastodonApi { @Header("Authorization") auth: String, @Header(DOMAIN_HEADER) domain: String, ): NetworkResult + + @GET("api/v1/tags/{name}") + suspend fun tag(@Path("name") name: String): NetworkResult + + @POST("api/v1/tags/{name}/follow") + suspend fun followTag(@Path("name") name: String): NetworkResult + + @POST("api/v1/tags/{name}/unfollow") + suspend fun unfollowTag(@Path("name") name: String): NetworkResult } diff --git a/app/src/main/res/drawable/ic_person_remove_24dp.xml b/app/src/main/res/drawable/ic_person_remove_24dp.xml new file mode 100644 index 000000000..10332c28c --- /dev/null +++ b/app/src/main/res/drawable/ic_person_remove_24dp.xml @@ -0,0 +1,8 @@ + + + + \ 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 new file mode 100644 index 000000000..159593dc4 --- /dev/null +++ b/app/src/main/res/menu/view_hashtag_toolbar.xml @@ -0,0 +1,21 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7c163d188..21b0708ef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -22,6 +22,8 @@ Images and videos cannot both be attached to the same post. The upload failed. Error sending post. + Error following #%s + Error unfollowing #%s Login Home From d5b3b2088f63873e9f101c15729b673b6a94a83f Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 7 Aug 2022 19:13:34 +0200 Subject: [PATCH 022/142] update Android Gradle Plugin to 7.2.2 (#2639) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 725ab8da4..fb0868bc3 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { gradlePluginPortal() } dependencies { - classpath "com.android.tools.build:gradle:7.2.0" + classpath "com.android.tools.build:gradle:7.2.2" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21" classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.1" } From 8ae3e4e1731de235dd2dca6f6a709ca96971a52b Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 7 Aug 2022 19:13:44 +0200 Subject: [PATCH 023/142] update Gradle to 7.5.1 (#2640) * update Gradle to 7.5 * update gradle to 7.5.1 --- gradle/wrapper/gradle-wrapper.jar | Bin 59536 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 16 +++++++++++----- gradlew.bat | 14 ++++++++------ 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7454180f2ae8848c63b8b4dea2cb829da983f2fa..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch delta 10158 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 delta 9041 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d7e66b5c6..8fad3f5a9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 3da45c161..a69d9cb6c 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ #!/bin/sh # -# Copyright ? 2015-2021 the original authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -32,10 +32,10 @@ # Busybox and similar reduced shells will NOT work, because this script # requires all of these POSIX shell features: # * functions; -# * expansions ?$var?, ?${var}?, ?${var:-default}?, ?${var+SET}?, -# ?${var#prefix}?, ?${var%suffix}?, and ?$( cmd )?; -# * compound commands having a testable exit status, especially ?case?; -# * various built-in commands including ?command?, ?set?, and ?ulimit?. +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». # # Important for patching: # @@ -205,6 +205,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f93..53a6b238d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From 68c9870b19858a9bfef75c33ef7d51d7d315853a Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 7 Aug 2022 19:13:59 +0200 Subject: [PATCH 024/142] update AndroidX dependencies (#2641) * update AndroidX dependencies * fix ComposeActivityTest --- app/build.gradle | 16 ++++++++-------- .../receiver/SendStatusBroadcastReceiver.kt | 2 +- .../keylesspalace/tusky/ComposeActivityTest.kt | 3 ++- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 730d428a9..f06052bcd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -93,8 +93,8 @@ android { } ext.coroutinesVersion = "1.6.1" -ext.lifecycleVersion = "2.4.1" -ext.roomVersion = '2.4.2' +ext.lifecycleVersion = "2.5.1" +ext.roomVersion = '2.4.3' ext.retrofitVersion = '2.9.0' ext.okhttpVersion = '4.9.3' ext.glideVersion = '4.13.1' @@ -108,9 +108,9 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$coroutinesVersion" - implementation "androidx.core:core-ktx:1.7.0" - implementation "androidx.appcompat:appcompat:1.4.1" - implementation "androidx.fragment:fragment-ktx:1.4.1" + implementation "androidx.core:core-ktx:1.8.0" + implementation "androidx.appcompat:appcompat:1.4.2" + implementation "androidx.fragment:fragment-ktx:1.5.1" implementation "androidx.browser:browser:1.4.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0" implementation "androidx.recyclerview:recyclerview:1.2.1" @@ -125,16 +125,16 @@ dependencies { implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion" - implementation "androidx.constraintlayout:constraintlayout:2.1.3" + implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.paging:paging-runtime-ktx:3.1.1" implementation "androidx.viewpager2:viewpager2:1.0.0" implementation "androidx.work:work-runtime:2.7.1" implementation "androidx.room:room-ktx:$roomVersion" implementation "androidx.room:room-paging:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion" - implementation 'androidx.core:core-splashscreen:1.0.0-beta02' + implementation 'androidx.core:core-splashscreen:1.0.0' - implementation "com.google.android.material:material:1.6.0" + implementation "com.google.android.material:material:1.6.1" implementation "com.google.code.gson:gson:2.9.0" diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt index 53cccf09d..75ccf799a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -126,6 +126,6 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { private fun getReplyMessage(intent: Intent): CharSequence { val remoteInput = RemoteInput.getResultsFromIntent(intent) - return remoteInput.getCharSequence(NotificationHelper.KEY_REPLY, "") + return remoteInput?.getCharSequence(NotificationHelper.KEY_REPLY, "") ?: "" } } diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index f041f57b6..8dd92162d 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -42,6 +42,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.robolectric.Robolectric import org.robolectric.Shadows.shadowOf @@ -131,7 +132,7 @@ class ComposeActivityTest { } val viewModelFactoryMock: ViewModelFactory = mock { - on { create(ComposeViewModel::class.java) } doReturn viewModel + on { create(eq(ComposeViewModel::class.java), any()) } doReturn viewModel } activity.accountManager = accountManagerMock From 17cfa3d9b4b0c4ea92dfa06a3b77dafaf792a571 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 7 Aug 2022 19:14:42 +0200 Subject: [PATCH 025/142] support Pleroma upload_limit configuration (#2646) * support Pleroma upload_limit configuration * fix ComposeActivityTest --- .../tusky/components/instanceinfo/InstanceInfoRepository.kt | 4 ++-- app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt | 3 ++- .../test/java/com/keylesspalace/tusky/ComposeActivityTest.kt | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt index 7415dd06b..170cff2b9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt @@ -70,8 +70,8 @@ class InstanceInfoRepository @Inject constructor( maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration, charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl, version = instance.version, - videoSizeLimit = instance.configuration?.mediaAttachments?.videoSizeLimit, - imageSizeLimit = instance.configuration?.mediaAttachments?.imageSizeLimit, + videoSizeLimit = instance.configuration?.mediaAttachments?.videoSizeLimit ?: instance.uploadLimit, + imageSizeLimit = instance.configuration?.mediaAttachments?.imageSizeLimit ?: instance.uploadLimit, imageMatrixLimit = instance.configuration?.mediaAttachments?.imageMatrixLimit, maxMediaAttachments = instance.configuration?.statuses?.maxMediaAttachments ?: instance.maxMediaAttachments, maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields, diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt index eccc86967..5cd921b84 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt @@ -32,7 +32,8 @@ data class Instance( @SerializedName("poll_limits") val pollConfiguration: PollConfiguration?, val configuration: InstanceConfiguration?, @SerializedName("max_media_attachments") val maxMediaAttachments: Int?, - val pleroma: PleromaConfiguration? + val pleroma: PleromaConfiguration?, + @SerializedName("upload_limit") val uploadLimit: Int? ) { override fun hashCode(): Int { return uri.hashCode() diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index 8dd92162d..2c20cb78e 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -465,7 +465,8 @@ class ComposeActivityTest { pollConfiguration = null, configuration = configuration, maxMediaAttachments = null, - pleroma = null + pleroma = null, + uploadLimit = null ) } From 12e42e9b2b2e187686a42a8a54f5d794f2399c9b Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 7 Aug 2022 19:15:21 +0200 Subject: [PATCH 026/142] update ktlint gradle plugin to 10.3.0 (#2649) --- build.gradle | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/build.gradle b/build.gradle index fb0868bc3..28f8fae19 100644 --- a/build.gradle +++ b/build.gradle @@ -7,12 +7,9 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:7.2.2" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21" - classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.1" + classpath "org.jlleitschuh.gradle:ktlint-gradle:10.3.0" } } -plugins { - id "org.jlleitschuh.gradle.ktlint" version "10.2.1" -} allprojects { apply plugin: "org.jlleitschuh.gradle.ktlint" From 4f0f9a7a12b669f20c6eb09a2109e986538ccdb4 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 7 Aug 2022 19:36:09 +0200 Subject: [PATCH 027/142] update Kotlin to 1.7.10 and fix some (new?) warnings (#2647) * update Kotlin to 1.7.10 and fix some (new?) warnings * remove unused import --- .../components/compose/ComposeTokenizer.kt | 4 +- .../compose/dialog/AddPollDialog.kt | 2 +- .../components/search/SearchViewModel.kt | 4 -- .../fragments/SearchStatusesFragment.kt | 2 +- .../tusky/fragment/ViewVideoFragment.kt | 3 +- .../util/ListStatusAccessibilityDelegate.kt | 2 +- .../com/keylesspalace/tusky/util/ListUtils.kt | 4 -- .../keylesspalace/tusky/util/MediaUtils.kt | 38 ------------------- .../tusky/util/StatusViewHelper.kt | 2 +- .../tusky/util/ViewBindingExtensions.kt | 19 +++++----- build.gradle | 2 +- 11 files changed, 17 insertions(+), 65 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeTokenizer.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeTokenizer.kt index 7b3d208b9..99e68db7d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeTokenizer.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeTokenizer.kt @@ -97,11 +97,11 @@ class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer { return if (i > 0 && text[i - 1] == ' ') { text } else if (text is Spanned) { - val s = SpannableString(text.toString() + " ") + val s = SpannableString("$text ") TextUtils.copySpansFrom(text, 0, text.length, Object::class.java, s, 0) s } else { - text.toString() + " " + "$text " } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt index a87b1b238..005e67297 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt @@ -77,7 +77,7 @@ fun showAddPollDialog( } val pollDurationId = durations.indexOfLast { - it <= poll?.expiresIn ?: 0 + it <= (poll?.expiresIn ?: 0) } binding.pollDurationSpinner.setSelection(pollDurationId) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index af886cdd1..738c662ab 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -191,10 +191,6 @@ class SearchViewModel @Inject constructor( .autoDispose() } - fun getAllAccountsOrderedByActive(): List { - return accountManager.getAllAccountsOrderedByActive() - } - fun muteAccount(accountId: String, notifications: Boolean, duration: Int?) { timelineCases.mute(accountId, notifications, duration) } 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 2e7849c1d..63bc0b648 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 @@ -288,7 +288,7 @@ class SearchStatusesFragment : SearchFragment(), Status val stringToShare = statusToShare.account.username + " - " + - statusToShare.content.toString() + statusToShare.content sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare) sendIntent.type = "text/plain" startActivity(Intent.createChooser(sendIntent, resources.getText(R.string.send_post_content_to))) 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 a1930da8d..214741a8e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt @@ -171,12 +171,11 @@ class ViewVideoFragment : ViewMediaFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val attachment = arguments?.getParcelable(ARG_ATTACHMENT) - val url: String if (attachment == null) { throw IllegalArgumentException("attachment has to be set") } - url = attachment.url + val url = attachment.url isAudio = attachment.type == Attachment.Type.AUDIO finalizeViewSetup(url, attachment.previewUrl, attachment.description) } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt index 501f90565..8540b013b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt @@ -48,7 +48,7 @@ class ListStatusAccessibilityDelegate( val pos = recyclerView.getChildAdapterPosition(host) val status = statusProvider.getStatus(pos) ?: return if (status is StatusViewData.Concrete) { - if (!status.spoilerText.isNullOrEmpty()) { + if (status.spoilerText.isNotEmpty()) { info.addAction(if (status.isExpanded) collapseCwAction else expandCwAction) } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt index 7cdc12e83..e7f03fca4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt @@ -52,7 +52,3 @@ inline fun List.replacedFirstWhich(replacement: T, predicate: (T) -> Bool } return newList } - -inline fun Iterable<*>.firstIsInstanceOrNull(): R? { - return firstOrNull { it is R }?.let { it as R } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt index 5482b292b..9568d73df 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt @@ -23,7 +23,6 @@ import android.graphics.Matrix import android.net.Uri import android.provider.OpenableColumns import android.util.Log -import androidx.annotation.Px import androidx.exifinterface.media.ExifInterface import java.io.File import java.io.FileNotFoundException @@ -68,43 +67,6 @@ fun getMediaSize(contentResolver: ContentResolver, uri: Uri?): Long { return mediaSize } -fun getSampledBitmap(contentResolver: ContentResolver, uri: Uri, @Px reqWidth: Int, @Px reqHeight: Int): Bitmap? { - // First decode with inJustDecodeBounds=true to check dimensions - val options = BitmapFactory.Options() - options.inJustDecodeBounds = true - var stream: InputStream? - try { - stream = contentResolver.openInputStream(uri) - } catch (e: FileNotFoundException) { - Log.w(TAG, e) - return null - } - - BitmapFactory.decodeStream(stream, null, options) - - IOUtils.closeQuietly(stream) - - // Calculate inSampleSize - options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight) - - // Decode bitmap with inSampleSize set - options.inJustDecodeBounds = false - return try { - stream = contentResolver.openInputStream(uri) - val bitmap = BitmapFactory.decodeStream(stream, null, options) - val orientation = getImageOrientation(uri, contentResolver) - reorientBitmap(bitmap, orientation) - } catch (e: FileNotFoundException) { - Log.w(TAG, e) - null - } catch (e: OutOfMemoryError) { - Log.e(TAG, "OutOfMemoryError while trying to get sampled Bitmap", e) - null - } finally { - IOUtils.closeQuietly(stream) - } -} - @Throws(FileNotFoundException::class) fun getImageSquarePixels(contentResolver: ContentResolver, uri: Uri): Long { val input = contentResolver.openInputStream(uri) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt index 44d9fee3b..253ea7a0a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt @@ -166,7 +166,7 @@ class StatusViewHelper(private val itemView: View) { mediaPreviews[3].layoutParams.height = mediaPreviewHeight } } - if (attachments.isNullOrEmpty()) { + if (attachments.isEmpty()) { sensitiveMediaWarning.visibility = View.GONE sensitiveMediaShow.visibility = View.GONE } else { diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt index e2db79c6e..5342cbf33 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt @@ -32,17 +32,16 @@ class FragmentViewBindingDelegate( object : DefaultLifecycleObserver { override fun onCreate(owner: LifecycleOwner) { fragment.viewLifecycleOwnerLiveData.observe( - fragment, - { t -> - t?.lifecycle?.addObserver( - object : DefaultLifecycleObserver { - override fun onDestroy(owner: LifecycleOwner) { - binding = null - } + fragment + ) { t -> + t?.lifecycle?.addObserver( + object : DefaultLifecycleObserver { + override fun onDestroy(owner: LifecycleOwner) { + binding = null } - ) - } - ) + } + ) + } } } ) diff --git a/build.gradle b/build.gradle index 28f8fae19..716192653 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { classpath "com.android.tools.build:gradle:7.2.2" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.10" classpath "org.jlleitschuh.gradle:ktlint-gradle:10.3.0" } } From 1c11671f3ea4c42fa1ee94aacf4b237b2a523199 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 7 Aug 2022 19:36:20 +0200 Subject: [PATCH 028/142] update kotlin coroutines to 1.6.4 (#2648) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f06052bcd..d44a12f4b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -92,7 +92,7 @@ android { } } -ext.coroutinesVersion = "1.6.1" +ext.coroutinesVersion = "1.6.4" ext.lifecycleVersion = "2.5.1" ext.roomVersion = '2.4.3' ext.retrofitVersion = '2.9.0' From 1fed84f948bbfc13ca459545c06c106eb0fc24d6 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 7 Aug 2022 22:18:53 +0200 Subject: [PATCH 029/142] fix caching of instance defaults and emojis (#2643) * fix caching of instance defaults and emojis * use correct OnConflictStrategy * rename dao methods --- .../instanceinfo/InstanceInfoRepository.kt | 4 +-- .../com/keylesspalace/tusky/db/InstanceDao.kt | 30 ++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt index 170cff2b9..bfc5a9b8c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt @@ -45,7 +45,7 @@ class InstanceInfoRepository @Inject constructor( */ suspend fun getEmojis(): List = withContext(Dispatchers.IO) { api.getCustomEmojis() - .onSuccess { emojiList -> dao.insertOrReplace(EmojisEntity(instanceName, emojiList)) } + .onSuccess { emojiList -> dao.upsert(EmojisEntity(instanceName, emojiList)) } .getOrElse { throwable -> Log.w(TAG, "failed to load custom emojis, falling back to cache", throwable) dao.getEmojiInfo(instanceName)?.emojiList.orEmpty() @@ -78,7 +78,7 @@ class InstanceInfoRepository @Inject constructor( maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength, maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength ) - dao.insertOrReplace(instanceEntity) + dao.upsert(instanceEntity) instanceEntity }, { throwable -> diff --git a/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt index 3687da09e..0bf1dc32c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt @@ -20,15 +20,37 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.RewriteQueriesToDropUnusedColumns +import androidx.room.Transaction +import androidx.room.Update @Dao interface InstanceDao { - @Insert(onConflict = OnConflictStrategy.REPLACE, entity = InstanceEntity::class) - suspend fun insertOrReplace(instance: InstanceInfoEntity) + @Insert(onConflict = OnConflictStrategy.IGNORE, entity = InstanceEntity::class) + suspend fun insertOrIgnore(instance: InstanceInfoEntity): Long - @Insert(onConflict = OnConflictStrategy.REPLACE, entity = InstanceEntity::class) - suspend fun insertOrReplace(emojis: EmojisEntity) + @Update(onConflict = OnConflictStrategy.IGNORE, entity = InstanceEntity::class) + suspend fun updateOrIgnore(instance: InstanceInfoEntity) + + @Transaction + suspend fun upsert(instance: InstanceInfoEntity) { + if (insertOrIgnore(instance) == -1L) { + updateOrIgnore(instance) + } + } + + @Insert(onConflict = OnConflictStrategy.IGNORE, entity = InstanceEntity::class) + suspend fun insertOrIgnore(emojis: EmojisEntity): Long + + @Update(onConflict = OnConflictStrategy.IGNORE, entity = InstanceEntity::class) + suspend fun updateOrIgnore(emojis: EmojisEntity) + + @Transaction + suspend fun upsert(emojis: EmojisEntity) { + if (insertOrIgnore(emojis) == -1L) { + updateOrIgnore(emojis) + } + } @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1") From 3065eae9f2c921ea99e018f7ffa28b635736d1bb Mon Sep 17 00:00:00 2001 From: joenepraat Date: Mon, 8 Aug 2022 08:54:41 +0000 Subject: [PATCH 030/142] Translated using Weblate (Dutch) Currently translated at 99.5% (485 of 487 strings) Co-authored-by: joenepraat Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nl/ Translation: Tusky/Tusky --- app/src/main/res/values-nl/strings.xml | 153 ++++++++++++++----------- 1 file changed, 89 insertions(+), 64 deletions(-) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 2716427fd..b68ece145 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -9,22 +9,22 @@ Er deed zich een onbekende autorisatiefout voor. Autorisatie werd geweigerd. Kon geen inlogsleutel verkrijgen. - Tekst van deze toot is te lang! + Tekst van dit bericht is te lang! Bestandstype kan niet worden geüpload. Bestand kon niet worden geopend. Er is toestemming nodig om deze media te lezen. Er is toestemming nodig om media op te slaan. - Afbeeldingen en video\'s kunnen niet allebei aan dezelfde toot worden toegevoegd. + Afbeeldingen en video\'s kunnen niet allebei aan hetzelfde bericht worden toegevoegd. Uploaden mislukt. - Fout tijdens verzenden toot. + Fout tijdens verzenden bericht. Start Meldingen Lokaal Globaal Directe berichten Tabs - Toot - Toots + Gesprek + Berichten Met reacties Vastgezet Volgend @@ -47,8 +47,8 @@ Inklappen Hier is niets. Niets te zien. Swipe naar beneden om te verversen! - %s boostte jouw toot - %s markeerde jouw toot als favoriet + %s boostte jouw bericht + %s markeerde jouw bericht als favoriet %s volgt jou Rapporteer @%s Extra opmerkingen\? @@ -59,7 +59,7 @@ Als favoriet markeren Favoriet verwijderen Meer - Toot schrijven + Bericht schrijven Aanmelden Afmelden Ben je er zeker van dat je het account %1$s wil afmelden? @@ -71,8 +71,8 @@ Boosts tonen Rapporteren Verwijderen - TOOT - TOOT! + Toot! + Toot! Opnieuw proberen Sluiten Profiel @@ -100,7 +100,7 @@ Afwijzen Zoeken Concepten - Zichtbaarheid toot + Zichtbaarheid bericht Tekstwaarschuwing Emojis Tab toevoegen @@ -118,8 +118,8 @@ Link kopiëren Als %s openen Delen als … - Link van de toot delen - Inhoud van de toot delen met… + Link van het bericht delen met… + Inhoud van het bericht delen met… Media delen met … Verzonden! Gebruiker is gedeblokkeerd @@ -150,7 +150,7 @@ Downloaden Het volgverzoek intrekken? Dit account ontvolgen? - Deze toot verwijderen? + Dit bericht verwijderen\? Openbaar: op openbare tijdlijnen tonen Minder openbaar: niet op openbare tijdlijnen tonen Alleen volgers: alleen aan jouw volgers tonen @@ -164,8 +164,8 @@ Waarschuw mij wanneer ik word vermeld ik word gevolgd - mijn toots werden geboost - mijn toots als favoriet werden gemarkeerd + mijn berichten werden geboost + mijn berichten zijn als favoriet gemarkeerd Uiterlijk Thema Tijdlijnen @@ -176,7 +176,7 @@ Systeemthema gebruiken Webbrowser Aangepaste tabbladen gebruiken - Verberg zwevende tootknop tijdens scrollen + Verberg zwevende knop om een bericht te schrijven tijdens het scrollen Taal Filteren Tijdlijnen @@ -188,14 +188,14 @@ HTTP-proxy inschakelen Serveradres van HTTP-proxy Poort van HTTP-proxy - Standaardzichtbaarheid van jouw toots + Standaardzichtbaarheid van jouw berichten Media altijd als gevoelig markeren Publiceren Synchroniseren Openbaar Minder openbaar Alleen volgers - Tekstgrootte van toots + Tekstgrootte van berichten Kleinst Klein Standaard @@ -206,9 +206,9 @@ Nieuwe volgers Meldingen over nieuwe volgers Boosts - Meldingen wanneer jouw toots worden geboost + Meldingen wanneer jouw berichten worden geboost Favorieten - Meldingen wanneer jouw toots als favoriet worden gemarkeerd + Meldingen wanneer jouw berichten als favoriet worden gemarkeerd %s vermeldde jou %1$s, %2$s, %3$s en %4$d anderen %1$s, %2$s en %3$s @@ -232,8 +232,8 @@ Foutmeldingen & nieuwe functies aanvragen:\n https://github.com/tuskyapp/Tusky/issues Tusky\'s profiel - Deel de inhoud van de toot - Deel de link van de toot + Inhoud van bericht delen + Link van het bericht delen Afbeeldingen Video Volgverzoek verzonden @@ -267,19 +267,19 @@ Account besloten maken Handmatige goedkeuring vereist voor volgers Concept bewaren? - Toot aan het verzenden… - Verzenden van toot mislukt - Toots aan het verzenden + Bericht wordt verzonden… + Verzenden van het bericht is mislukt + Berichten worden verzonden Verzenden geannuleerd - Een kopie van de toot werd opgeslagen als concept - Toot schrijven + Een kopie van het bericht werd als concept opgeslagen + Bericht schrijven Jouw server %s heeft geen lokale emojis Emojistijl Systeemstandaard Je moet eerst deze emoji-sets downloaden Aan het zoeken… - Alle toots in- of uitklappen - Toot openen + Alle berichten in- of uitklappen + Bericht openen Herstarten app vereist Je moet Tusky herstarten om deze veranderingen te kunnen doorvoeren Later @@ -313,7 +313,7 @@ <b>%s</b> boosts Geboost door - Aan favorieten toegevoegd door + Als favoriet gemarkeerd door %1$s %1$s en %2$s %1$s, %2$s en %3$d meer @@ -321,16 +321,11 @@ maximum van %1$d tab bereikt maximum van %1$d tabs bereikt - Media: %s - - Inhoudswaarschuwing: %s - - Geen omschrijving - - Geboost - - Aan favorieten toegevoegd - + Media: %s + Inhoudswaarschuwing: %s + Geen omschrijving + Geboost + Als favoriet gemarkeerd Openbaar Minder openbaar @@ -362,11 +357,11 @@ Naam van lijst Hashtag zonder # Verwijderen en herschrijven - Deze toot verwijderen en herschrijven\? + Dit bericht verwijderen en herschrijven\? Leegmaken Filter Toepassen - Toot schrijven + Bericht schrijven Schrijven Botsindicator tonen Weet je zeker dat je alle meldingen permanent wilt verwijderen\? @@ -416,7 +411,7 @@ Extra opmerkingen Verder naar %s Het rapporteren is mislukt - Het ophalen van toots is mislukt + Het ophalen van berichten is mislukt Deze rapportage wordt naar jouw servermoderator(en) gestuurd. Je kunt hieronder een uitleg geven over waarom je het account wilt rapporteren: Het account is van een andere server. Wil je ook een geanonimiseerde kopie van de rapportage daarnaartoe sturen\? Meldingenfilter tonen @@ -434,25 +429,25 @@ Keuze %d Bewerken Bladwijzers - Ingeplande toots + Ingeplande berichten Bladwijzer Bewerken Bladwijzers Poll toevoegen - Ingeplande toots - Ingeplande toot + Ingeplande berichten + Ingepland bericht Herstellen Powered by Tusky - Altijd toots met tekstwaarschuwingen uitklappen + Berichten met tekstwaarschuwingen altijd uitklappen Als bladwijzer toegevoegd Kies een lijst Lijst Accounts Zoeken mislukt Poll - Fout tijdens opzoeken toot %s - Je hebt nog geen concepten - Je hebt nog geen ingeplande toots + Fout tijdens het opzoeken van bericht %s + Je hebt nog geen concepten. + Je hebt nog geen ingeplande berichten. Om in te plannen moet je in Mastodon een minimum interval van 5 minuten gebruiken. Volgverzoeken Hashtags @@ -460,8 +455,8 @@ volgverzoek verstuurd Afmelden Abonneren - De toot waarvoor jij een reactie had opgesteld, is verwijderd - Het versturen van deze toot is mislukt! + Het bericht waarvoor jij een reactie had opgesteld, is verwijderd + Het versturen van dit bericht is mislukt! Weet je zeker dat je de lijst %s wilt verwijderen\? Je kan niet meer dan %1$d mediabijlage uploaden. @@ -472,7 +467,7 @@ Jouw eigen opmerking over dit account Welzijn De titel van de bovenste statusbalk verbergen - Vraag voor het boosten van een toot een bevestiging + Vraag voor het boosten van een bericht een bevestiging Linkpreviews in tijdlijnen weergeven Er zijn geen aankondigingen. Oneindig @@ -484,14 +479,14 @@ Hashtag toevoegen Geluid - Meldingen wanneer iemand waar je op bent geabonneerd een nieuwe toot plaatst - Nieuwe toots + Meldingen wanneer iemand waar je op bent geabonneerd een nieuw bericht plaatst + Nieuwe berichten Meldingen over volgverzoeken Onder Boven Lokale emojis animeren Kleurverloop weergeven voor verborgen media - iemand waar ik op ben geabonneerd heeft een nieuwe toot geplaatst + iemand waar ik op ben geabonneerd heeft een nieuw bericht geplaatst Meldingen verbergen \@%s negeren\? \@%s blokkeren\? @@ -501,23 +496,53 @@ Meldingen van %s negeren Meldingen van %s niet meer negeren %s niet meer negeren - %s heeft net een toot geplaatst + %s heeft zojuist een bericht geplaatst %s verzoekt u te volgen Aankondigingen Meldingen beoordelen Concept verwijderd - Kwantitatieve statistieken voor toots verbergen + Kwantitatieve statistieken voor berichten verbergen Laden van reactie-informatie mislukt Kwantitatieve statistieken in profielen verbergen Hoofd navigatiepositie Dit gesprek verwijderen\? Gesprek verwijderen Ook al heb je geen besloten account, de medewerkers van %1$s dachten dat je misschien de volgverzoeken van deze accounts handmatig zou willen controleren. - Bepaalde informatie die invloed kan hebben op uw geestelijk welzijn zal worden verborgen. Dit bevat onder andere: + Bepaalde informatie die invloed kan hebben op jouw geestelijk welzijn zal worden verborgen. Dit bevat onder andere: \n -\n- Favoriet/Boost/Volg notificaties -\n- Favoriet/Boost/Aantal boosts per toot -\n- Volger/Bericht statistieken op profielen +\n- Meldingen over favorieten, boosts en volgers +\n- Weergave van het aantal favorieten en boosts per bericht +\n- Statistieken over het aantal volgers en berichten op profielen \n -\nPush-notificaties zullen niet worden beïnvloed, maar uw kunt uw notificatie voorkeuren handmatig wijzigen. +\nPushmeldingen worden hierdoor niet beïnvloed, maar je kunt de voorkeuren voor meldingen handmatig wijzigen. + %s heeft zich geregistreerd + Alle accounts opnieuw inloggen i.v.m. ondersteuning pushmeldingen + Afbeeldingen en video\'s kunnen niet groter zijn dan %s MB. + Deze afbeelding kon niet worden bewerkt. + Inloggen + Opnieuw inloggen i.v.m. pushmeldingen + %s heeft diens bericht bewerkt + Bladwijzer verwijderen + Afwijzen + Details + iemand heeft zich geregistreerd + een bericht waarmee ik interactie had is bewerkt + Registraties + Meldingen over nieuwe gebruikers + Bewerkingen van berichten + Meldingen wanneer berichten waarmee je interactie had werden bewerkt + 1+ + Afbeelding bewerken + 30 dagen + 60 dagen + 14 dagen + 90 dagen + 180 dagen + 365 dagen + Vraag voor het markeren als favoriet een bevestiging + Bericht schrijven + Geregistreerd op %1$s + Concept wordt opgeslagen… + Laden van accountdetails mislukt + De inlogpagina kon niet worden geladen. \ No newline at end of file From a5862c35478fe6bd3876685a4ab80d0b35e71bed Mon Sep 17 00:00:00 2001 From: GunChleoc Date: Mon, 8 Aug 2022 08:54:41 +0000 Subject: [PATCH 031/142] Translated using Weblate (Gaelic) Currently translated at 99.5% (487 of 489 strings) Co-authored-by: GunChleoc Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gd/ Translation: Tusky/Tusky --- app/src/main/res/values-gd/strings.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 9aa460765..c5b76f0f2 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -560,4 +560,9 @@ Clàraich a-steach às ùr airson brathan putaidh 1+ Deasaich an dealbh + Mearachd a’ leantainn air #%s + Chan fhaod na faidhlichean video ’ fuaime a bhith nas motha na %s MB. + Mearachd a’ sgur de leantainn air #%s + Mearachd a’ luchdadh fiosrachadh a’ chunntais + Cha b’ urrainn dhuinn an dealbh a dheasachadh. \ No newline at end of file From 4826d17bed56563021f7bfc5f7e3fdd543be1398 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 8 Aug 2022 08:54:41 +0000 Subject: [PATCH 032/142] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (489 of 489 strings) Co-authored-by: Eric Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/ Translation: Tusky/Tusky --- app/src/main/res/values-zh-rCN/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6b5a10c15..f9154ac80 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -545,4 +545,6 @@ 无法编辑图片。 加载账户详情失败 音视频文件大小不能超出 %s MB。 + 关注 #%s 出错 + 取关 #%s 出错 \ No newline at end of file From 607f448eb3bb0ff6d218b33ee58cfb99ec1b3391 Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Mon, 8 Aug 2022 11:02:22 +0200 Subject: [PATCH 033/142] =?UTF-8?q?Rename=20norsk=20bokm=C3=A5l=20translat?= =?UTF-8?q?ion=20from=20no=5FNB=20to=20nb=5FNO=20(#2652)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename norsk bokmål translation from no_NB to nb_NO * Revert locale name migration --- app/src/main/res/{values-no-rNB => values-nb-rNO}/strings.xml | 0 app/src/main/res/values/donottranslate.xml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename app/src/main/res/{values-no-rNB => values-nb-rNO}/strings.xml (100%) diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml similarity index 100% rename from app/src/main/res/values-no-rNB/strings.xml rename to app/src/main/res/values-nb-rNO/strings.xml diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index ed1e30cb6..5232e4e5f 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -103,7 +103,7 @@ it hu nl - no-nb + nb-no oc pl pt-BR From 741461acde9ed397fa4bbb291a261b206ceeddf3 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 15 Aug 2022 11:00:18 +0200 Subject: [PATCH 034/142] rewrite threads with Kotlin & coroutines (#2617) * initial class setup * handle events and filters * handle status state changes * code formatting * fix status filtering * cleanup code a bit * implement removeAllByAccountId * move toolbar into fragment, implement menu * error and load state handling * fix pull to refresh * implement reveal button * use requireContext() instead of context!! * jump to detailed status * add ViewThreadViewModelTest * fix ktlint * small code improvements (thx charlag) * add testcase for toggleRevealButton * add more state change testcases to ViewThreadViewModel --- app/src/main/AndroidManifest.xml | 2 +- .../tusky/BottomSheetActivity.kt | 1 + .../keylesspalace/tusky/ViewMediaActivity.kt | 1 + .../tusky/ViewThreadActivity.java | 130 ---- .../adapter/StatusDetailedViewHolder.java | 10 +- .../tusky/adapter/ThreadAdapter.kt | 129 ---- .../ConversationLineItemDecoration.kt | 20 +- .../components/viewthread/ThreadAdapter.kt | 95 +++ .../viewthread/ViewThreadActivity.kt | 62 ++ .../viewthread/ViewThreadFragment.kt | 337 +++++++++ .../viewthread/ViewThreadViewModel.kt | 426 +++++++++++ .../tusky/di/ActivitiesModule.kt | 2 +- .../tusky/di/FragmentBuildersModule.kt | 2 +- .../tusky/di/ViewModelFactory.kt | 5 + .../tusky/fragment/ViewThreadFragment.java | 683 ------------------ .../tusky/network/MastodonApi.kt | 11 +- .../keylesspalace/tusky/util/ViewDataUtils.kt | 4 +- .../tusky/viewdata/StatusViewData.kt | 1 + app/src/main/res/drawable/ic_back.xml | 12 + .../layout-sw640dp/fragment_view_thread.xml | 41 +- .../main/res/layout/activity_view_thread.xml | 5 +- .../main/res/layout/fragment_view_thread.xml | 57 +- .../tusky/components/timeline/StatusMocker.kt | 53 +- .../viewthread/ViewThreadViewModelTest.kt | 356 +++++++++ 24 files changed, 1446 insertions(+), 999 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/ViewThreadActivity.java delete mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.kt rename app/src/main/java/com/keylesspalace/tusky/{view => components/viewthread}/ConversationLineItemDecoration.kt (78%) create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadActivity.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java create mode 100644 app/src/main/res/drawable/ic_back.xml create mode 100644 app/src/test/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModelTest.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2273316d8..349686902 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -98,7 +98,7 @@ android:theme="@style/TuskyDialogActivityTheme" android:windowSoftInputMode="stateVisible|adjustResize" /> . */ - -package com.keylesspalace.tusky; - -import android.content.Context; -import android.content.Intent; -import android.os.Bundle; -import androidx.annotation.Nullable; -import androidx.fragment.app.FragmentTransaction; -import androidx.appcompat.app.ActionBar; -import androidx.appcompat.widget.Toolbar; -import android.view.Menu; -import android.view.MenuItem; - -import com.keylesspalace.tusky.fragment.ViewThreadFragment; -import com.keylesspalace.tusky.util.LinkHelper; - -import javax.inject.Inject; - -import dagger.android.AndroidInjector; -import dagger.android.DispatchingAndroidInjector; -import dagger.android.HasAndroidInjector; - -public class ViewThreadActivity extends BottomSheetActivity implements HasAndroidInjector { - - public static final int REVEAL_BUTTON_HIDDEN = 1; - public static final int REVEAL_BUTTON_REVEAL = 2; - public static final int REVEAL_BUTTON_HIDE = 3; - - public static Intent startIntent(Context context, String id, String url) { - Intent intent = new Intent(context, ViewThreadActivity.class); - intent.putExtra(ID_EXTRA, id); - intent.putExtra(URL_EXTRA, url); - return intent; - } - - private static final String ID_EXTRA = "id"; - private static final String URL_EXTRA = "url"; - private static final String FRAGMENT_TAG = "ViewThreadFragment_"; - - private int revealButtonState = REVEAL_BUTTON_HIDDEN; - - @Inject - public DispatchingAndroidInjector dispatchingAndroidInjector; - - private ViewThreadFragment fragment; - - @Override - protected void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_view_thread); - - Toolbar toolbar = findViewById(R.id.toolbar); - setSupportActionBar(toolbar); - ActionBar actionBar = getSupportActionBar(); - if (actionBar != null) { - actionBar.setTitle(R.string.title_view_thread); - actionBar.setDisplayHomeAsUpEnabled(true); - actionBar.setDisplayShowHomeEnabled(true); - } - - String id = getIntent().getStringExtra(ID_EXTRA); - - fragment = (ViewThreadFragment)getSupportFragmentManager().findFragmentByTag(FRAGMENT_TAG + id); - if(fragment == null) { - fragment = ViewThreadFragment.newInstance(id); - } - - FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); - fragmentTransaction.replace(R.id.fragment_container, fragment, FRAGMENT_TAG + id); - fragmentTransaction.commit(); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.view_thread_toolbar, menu); - MenuItem menuItem = menu.findItem(R.id.action_reveal); - menuItem.setVisible(revealButtonState != REVEAL_BUTTON_HIDDEN); - menuItem.setIcon(revealButtonState == REVEAL_BUTTON_REVEAL ? - R.drawable.ic_eye_24dp : R.drawable.ic_hide_media_24dp); - return super.onCreateOptionsMenu(menu); - } - - public void setRevealButtonState(int state) { - switch (state) { - case REVEAL_BUTTON_HIDDEN: - case REVEAL_BUTTON_REVEAL: - case REVEAL_BUTTON_HIDE: - this.revealButtonState = state; - invalidateOptionsMenu(); - break; - default: - throw new IllegalArgumentException("Invalid reveal button state: " + state); - } - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_open_in_web: { - openLink(getIntent().getStringExtra(URL_EXTRA)); - return true; - } - case R.id.action_reveal: { - fragment.onRevealPressed(); - return true; - } - } - return super.onOptionsItemSelected(item); - } - - @Override - public AndroidInjector androidInjector() { - return dispatchingAndroidInjector; - } - -} 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 ae0b0678b..74f09f641 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -21,12 +21,12 @@ import com.keylesspalace.tusky.viewdata.StatusViewData; import java.text.DateFormat; import java.util.Date; -class StatusDetailedViewHolder extends StatusBaseViewHolder { - private TextView reblogs; - private TextView favourites; - private View infoDivider; +public class StatusDetailedViewHolder extends StatusBaseViewHolder { + private final TextView reblogs; + private final TextView favourites; + private final View infoDivider; - StatusDetailedViewHolder(View view) { + public StatusDetailedViewHolder(View view) { super(view); reblogs = view.findViewById(R.id.status_reblogs); favourites = view.findViewById(R.id.status_favourites); diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.kt deleted file mode 100644 index 8abbbd5f6..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/ThreadAdapter.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* 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.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.interfaces.StatusActionListener -import com.keylesspalace.tusky.util.StatusDisplayOptions -import com.keylesspalace.tusky.viewdata.StatusViewData - -class ThreadAdapter( - private val statusDisplayOptions: StatusDisplayOptions, - private val statusActionListener: StatusActionListener -) : RecyclerView.Adapter() { - private val statuses = mutableListOf() - var detailedStatusPosition: Int = RecyclerView.NO_POSITION - private set - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusBaseViewHolder { - return when (viewType) { - VIEW_TYPE_STATUS -> { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_status, parent, false) - StatusViewHolder(view) - } - VIEW_TYPE_STATUS_DETAILED -> { - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.item_status_detailed, parent, false) - StatusDetailedViewHolder(view) - } - else -> error("Unknown item type: $viewType") - } - } - - override fun onBindViewHolder(viewHolder: StatusBaseViewHolder, position: Int) { - val status = statuses[position] - viewHolder.setupWithStatus(status, statusActionListener, statusDisplayOptions) - } - - override fun getItemViewType(position: Int): Int { - return if (position == detailedStatusPosition) { - VIEW_TYPE_STATUS_DETAILED - } else { - VIEW_TYPE_STATUS - } - } - - override fun getItemCount(): Int = statuses.size - - fun setStatuses(statuses: List?) { - this.statuses.clear() - this.statuses.addAll(statuses!!) - notifyDataSetChanged() - } - - fun addItem(position: Int, statusViewData: StatusViewData.Concrete) { - statuses.add(position, statusViewData) - notifyItemInserted(position) - } - - fun clearItems() { - val oldSize = statuses.size - statuses.clear() - detailedStatusPosition = RecyclerView.NO_POSITION - notifyItemRangeRemoved(0, oldSize) - } - - fun addAll(position: Int, statuses: List) { - this.statuses.addAll(position, statuses) - notifyItemRangeInserted(position, statuses.size) - } - - fun addAll(statuses: List) { - val end = statuses.size - this.statuses.addAll(statuses) - notifyItemRangeInserted(end, statuses.size) - } - - fun removeItem(position: Int) { - statuses.removeAt(position) - notifyItemRemoved(position) - } - - fun clear() { - statuses.clear() - detailedStatusPosition = RecyclerView.NO_POSITION - notifyDataSetChanged() - } - - fun setItem(position: Int, status: StatusViewData.Concrete, notifyAdapter: Boolean) { - statuses[position] = status - if (notifyAdapter) { - notifyItemChanged(position) - } - } - - fun getItem(position: Int): StatusViewData.Concrete? = statuses.getOrNull(position) - - fun setDetailedStatusPosition(position: Int) { - if (position != detailedStatusPosition && - detailedStatusPosition != RecyclerView.NO_POSITION - ) { - val prior = detailedStatusPosition - detailedStatusPosition = position - notifyItemChanged(prior) - } else { - detailedStatusPosition = position - } - } - - companion object { - private const val VIEW_TYPE_STATUS = 0 - private const val VIEW_TYPE_STATUS_DETAILED = 1 - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ConversationLineItemDecoration.kt similarity index 78% rename from app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.kt rename to app/src/main/java/com/keylesspalace/tusky/components/viewthread/ConversationLineItemDecoration.kt index c291bd019..7e98ebef7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/ConversationLineItemDecoration.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ConversationLineItemDecoration.kt @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.view +package com.keylesspalace.tusky.components.viewthread import android.content.Context import android.graphics.Canvas @@ -22,7 +22,6 @@ import android.view.View import androidx.core.content.ContextCompat import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.adapter.ThreadAdapter class ConversationLineItemDecoration(private val context: Context) : RecyclerView.ItemDecoration() { @@ -39,22 +38,19 @@ class ConversationLineItemDecoration(private val context: Context) : RecyclerVie val child = parent.getChildAt(i) val position = parent.getChildAdapterPosition(child) - val adapter = parent.adapter as ThreadAdapter + val items = (parent.adapter as ThreadAdapter).currentList + + val current = items.getOrNull(position) - val current = adapter.getItem(position) - val dividerTop: Int - val dividerBottom: Int if (current != null) { - val above = adapter.getItem(position - 1) - dividerTop = if (above != null && above.id == current.status.inReplyToId) { + val above = items.getOrNull(position - 1) + val dividerTop = if (above != null && above.id == current.status.inReplyToId) { child.top } else { child.top + avatarMargin } - val below = adapter.getItem(position + 1) - dividerBottom = if (below != null && current.id == below.status.inReplyToId && - adapter.detailedStatusPosition != position - ) { + val below = items.getOrNull(position + 1) + val dividerBottom = if (below != null && current.id == below.status.inReplyToId && below.isDetailed) { child.bottom } else { child.top + avatarMargin diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt new file mode 100644 index 000000000..9e0903b06 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt @@ -0,0 +1,95 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.viewthread + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.adapter.StatusDetailedViewHolder +import com.keylesspalace.tusky.adapter.StatusViewHolder +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.StatusViewData + +class ThreadAdapter( + private val statusDisplayOptions: StatusDisplayOptions, + private val statusActionListener: StatusActionListener +) : ListAdapter(ThreadDifferCallback) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusBaseViewHolder { + return when (viewType) { + VIEW_TYPE_STATUS -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_status, parent, false) + StatusViewHolder(view) + } + VIEW_TYPE_STATUS_DETAILED -> { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_status_detailed, parent, false) + StatusDetailedViewHolder(view) + } + else -> error("Unknown item type: $viewType") + } + } + + override fun onBindViewHolder(viewHolder: StatusBaseViewHolder, position: Int) { + val status = getItem(position) + viewHolder.setupWithStatus(status, statusActionListener, statusDisplayOptions) + } + + override fun getItemViewType(position: Int): Int { + return if (getItem(position).isDetailed) { + VIEW_TYPE_STATUS_DETAILED + } else { + VIEW_TYPE_STATUS + } + } + + companion object { + private const val VIEW_TYPE_STATUS = 0 + private const val VIEW_TYPE_STATUS_DETAILED = 1 + + val ThreadDifferCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: StatusViewData.Concrete, + newItem: StatusViewData.Concrete + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: StatusViewData.Concrete, + newItem: StatusViewData.Concrete + ): Boolean { + return false // Items are different always. It allows to refresh timestamp on every view holder update + } + + override fun getChangePayload( + oldItem: StatusViewData.Concrete, + newItem: StatusViewData.Concrete + ): Any? { + return if (oldItem == newItem) { + // If items are equal - update timestamp only + listOf(StatusBaseViewHolder.Key.KEY_CREATED) + } else // If items are different - update the whole view holder + null + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadActivity.kt new file mode 100644 index 000000000..ed0393fab --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadActivity.kt @@ -0,0 +1,62 @@ +/* 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.components.viewthread + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.commit +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.R +import dagger.android.DispatchingAndroidInjector +import dagger.android.HasAndroidInjector +import javax.inject.Inject + +class ViewThreadActivity : BottomSheetActivity(), HasAndroidInjector { + + @Inject + lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_view_thread) + val id = intent.getStringExtra(ID_EXTRA)!! + val url = intent.getStringExtra(URL_EXTRA)!! + val fragment = + supportFragmentManager.findFragmentByTag(FRAGMENT_TAG + id) as ViewThreadFragment? + ?: ViewThreadFragment.newInstance(id, url) + + supportFragmentManager.commit { + replace(R.id.fragment_container, fragment, FRAGMENT_TAG + id) + } + } + + override fun androidInjector() = dispatchingAndroidInjector + + companion object { + + fun startIntent(context: Context, id: String, url: String): Intent { + val intent = Intent(context, ViewThreadActivity::class.java) + intent.putExtra(ID_EXTRA, id) + intent.putExtra(URL_EXTRA, url) + return intent + } + + private const val ID_EXTRA = "id" + private const val URL_EXTRA = "url" + private const val FRAGMENT_TAG = "ViewThreadFragment_" + } +} 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 new file mode 100644 index 000000000..cee6f0462 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt @@ -0,0 +1,337 @@ +/* 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.components.viewthread + +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.SimpleItemAnimator +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.AccountListActivity +import com.keylesspalace.tusky.AccountListActivity.Companion.newIntent +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.FragmentViewThreadBinding +import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.fragment.SFragment +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate +import com.keylesspalace.tusky.util.StatusDisplayOptions +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.viewdata.AttachmentViewData.Companion.list +import com.keylesspalace.tusky.viewdata.StatusViewData +import kotlinx.coroutines.launch +import java.io.IOException +import javax.inject.Inject + +class ViewThreadFragment : SFragment(), OnRefreshListener, StatusActionListener, Injectable { + + @Inject + lateinit var viewModelFactory: ViewModelFactory + + private val viewModel: ViewThreadViewModel by viewModels { viewModelFactory } + + private val binding by viewBinding(FragmentViewThreadBinding::bind) + + private lateinit var adapter: ThreadAdapter + private lateinit var thisThreadsStatusId: String + + private var alwaysShowSensitiveMedia = false + private var alwaysOpenSpoiler = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + thisThreadsStatusId = requireArguments().getString(ID_EXTRA)!! + val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + + val statusDisplayOptions = StatusDisplayOptions( + animateAvatars = preferences.getBoolean("animateGifAvatars", false), + mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled, + useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false), + showBotOverlay = preferences.getBoolean("showBotOverlay", true), + useBlurhash = preferences.getBoolean("useBlurhash", true), + cardViewMode = if (preferences.getBoolean("showCardsInTimelines", false)) { + CardViewMode.INDENTED + } else { + CardViewMode.NONE + }, + confirmReblogs = preferences.getBoolean("confirmReblogs", true), + confirmFavourites = preferences.getBoolean("confirmFavourites", false), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + ) + adapter = ThreadAdapter(statusDisplayOptions, this) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_view_thread, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + + binding.toolbar.setNavigationOnClickListener { + activity?.onBackPressed() + } + binding.toolbar.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.action_reveal -> { + viewModel.toggleRevealButton() + true + } + R.id.action_open_in_web -> { + context?.openLink(requireArguments().getString(URL_EXTRA)!!) + true + } + else -> false + } + } + + binding.swipeRefreshLayout.setOnRefreshListener(this) + binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) + + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(context) + binding.recyclerView.setAccessibilityDelegateCompat( + ListStatusAccessibilityDelegate( + binding.recyclerView, + this + ) { index -> adapter.currentList.getOrNull(index) } + ) + val divider = DividerItemDecoration(context, LinearLayout.VERTICAL) + binding.recyclerView.addItemDecoration(divider) + binding.recyclerView.addItemDecoration(ConversationLineItemDecoration(requireContext())) + alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia + alwaysOpenSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler + + binding.recyclerView.adapter = adapter + + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.uiState.collect { uiState -> + when (uiState) { + is ThreadUiState.Loading -> { + updateRevealButton(RevealButtonState.NO_BUTTON) + binding.recyclerView.hide() + binding.statusView.hide() + binding.progressBar.show() + } + is ThreadUiState.Error -> { + Log.w(TAG, "failed to load status", uiState.throwable) + + updateRevealButton(RevealButtonState.NO_BUTTON) + binding.swipeRefreshLayout.isRefreshing = false + + binding.recyclerView.hide() + binding.statusView.show() + binding.progressBar.hide() + + if (uiState.throwable is IOException) { + binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { + viewModel.retry(thisThreadsStatusId) + } + } else { + binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { + viewModel.retry(thisThreadsStatusId) + } + } + } + is ThreadUiState.Success -> { + adapter.submitList(uiState.statuses) { + if (viewModel.isInitialLoad) { + viewModel.isInitialLoad = false + val detailedPosition = adapter.currentList.indexOfFirst { viewData -> + viewData.isDetailed + } + binding.recyclerView.scrollToPosition(detailedPosition) + } + } + + updateRevealButton(uiState.revealButton) + binding.swipeRefreshLayout.isRefreshing = uiState.refreshing + + binding.recyclerView.show() + binding.statusView.hide() + binding.progressBar.hide() + } + } + } + } + + lifecycleScope.launch { + viewModel.errors.collect { throwable -> + Log.w(TAG, "failed to load status context", throwable) + Snackbar.make(binding.root, R.string.error_generic, Snackbar.LENGTH_SHORT) + .setAction(R.string.action_retry) { + viewModel.retry(thisThreadsStatusId) + } + .show() + } + } + + viewModel.loadThread(thisThreadsStatusId) + } + + private fun updateRevealButton(state: RevealButtonState) { + val menuItem = binding.toolbar.menu.findItem(R.id.action_reveal) + + menuItem.isVisible = state != RevealButtonState.NO_BUTTON + menuItem.setIcon(if (state == RevealButtonState.REVEAL) R.drawable.ic_eye_24dp else R.drawable.ic_hide_media_24dp) + } + + override fun onRefresh() { + viewModel.refresh(thisThreadsStatusId) + } + + override fun onReply(position: Int) { + super.reply(adapter.currentList[position].status) + } + + override fun onReblog(reblog: Boolean, position: Int) { + val status = adapter.currentList[position] + viewModel.reblog(reblog, status) + } + + override fun onFavourite(favourite: Boolean, position: Int) { + val status = adapter.currentList[position] + viewModel.favorite(favourite, status) + } + + override fun onBookmark(bookmark: Boolean, position: Int) { + val status = adapter.currentList[position] + viewModel.bookmark(bookmark, status) + } + + override fun onMore(view: View, position: Int) { + super.more(adapter.currentList[position].status, view, position) + } + + override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { + val status = adapter.currentList[position].status + super.viewMedia(attachmentIndex, list(status), view) + } + + override fun onViewThread(position: Int) { + val status = adapter.currentList[position] + if (thisThreadsStatusId == status.id) { + // If already viewing this thread, don't reopen it. + return + } + super.viewThread(status.actionableId, status.actionable.url) + } + + override fun onViewUrl(url: String) { + val status: StatusViewData.Concrete? = viewModel.detailedStatus() + if (status != null && status.status.url == url) { + // already viewing the status with this url + // probably just a preview federated and the user is clicking again to view more -> open the browser + // this can happen with some friendica statuses + requireContext().openLink(url) + return + } + super.onViewUrl(url) + } + + override fun onOpenReblog(position: Int) { + // there are no reblogs in threads + } + + override fun onExpandedChange(expanded: Boolean, position: Int) { + viewModel.changeExpanded(expanded, adapter.currentList[position]) + } + + override fun onContentHiddenChange(isShowing: Boolean, position: Int) { + viewModel.changeContentShowing(isShowing, adapter.currentList[position]) + } + + override fun onLoadMore(position: Int) { + // only used in timelines + } + + override fun onShowReblogs(position: Int) { + val statusId = adapter.currentList[position].id + val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId) + (requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent) + } + + override fun onShowFavs(position: Int) { + val statusId = adapter.currentList[position].id + val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId) + (requireActivity() as BaseActivity).startActivityWithSlideInAnimation(intent) + } + + override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { + viewModel.changeContentCollapsed(isCollapsed, adapter.currentList[position]) + } + + override fun onViewTag(tag: String) { + super.viewTag(tag) + } + + override fun onViewAccount(id: String) { + super.viewAccount(id) + } + + public override fun removeItem(position: Int) { + val status = adapter.currentList[position] + if (status.isDetailed) { + // the main status we are viewing is being removed, finish the activity + activity?.finish() + return + } + viewModel.removeStatus(status) + } + + override fun onVoteInPoll(position: Int, choices: List) { + val status = adapter.currentList[position] + viewModel.voteInPoll(choices, status) + } + + companion object { + private const val TAG = "ViewThreadFragment" + + private const val ID_EXTRA = "id" + private const val URL_EXTRA = "url" + + fun newInstance(id: String, url: String): ViewThreadFragment { + val arguments = Bundle(2) + val fragment = ViewThreadFragment() + arguments.putString(ID_EXTRA, id) + arguments.putString(URL_EXTRA, url) + fragment.arguments = arguments + return fragment + } + } +} 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 new file mode 100644 index 000000000..c109494cd --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt @@ -0,0 +1,426 @@ +/* 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.components.viewthread + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold +import at.connyduck.calladapter.networkresult.getOrElse +import com.keylesspalace.tusky.appstore.BlockEvent +import com.keylesspalace.tusky.appstore.BookmarkEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.FavoriteEvent +import com.keylesspalace.tusky.appstore.PinEvent +import com.keylesspalace.tusky.appstore.ReblogEvent +import com.keylesspalace.tusky.appstore.StatusComposedEvent +import com.keylesspalace.tusky.appstore.StatusDeletedEvent +import com.keylesspalace.tusky.components.timeline.util.ifExpected +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Filter +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.toViewData +import com.keylesspalace.tusky.viewdata.StatusViewData +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx3.asFlow +import kotlinx.coroutines.rx3.await +import javax.inject.Inject + +class ViewThreadViewModel @Inject constructor( + private val api: MastodonApi, + private val filterModel: FilterModel, + private val timelineCases: TimelineCases, + eventHub: EventHub, + accountManager: AccountManager +) : ViewModel() { + + private val _uiState: MutableStateFlow = MutableStateFlow(ThreadUiState.Loading) + val uiState: Flow + get() = _uiState + + private val _errors = MutableSharedFlow(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + val errors: Flow + get() = _errors + + var isInitialLoad: Boolean = true + + private val alwaysShowSensitiveMedia: Boolean + private val alwaysOpenSpoiler: Boolean + + init { + val activeAccount = accountManager.activeAccount + alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false + alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false + + viewModelScope.launch { + eventHub.events + .asFlow() + .collect { event -> + when (event) { + is FavoriteEvent -> handleFavEvent(event) + is ReblogEvent -> handleReblogEvent(event) + is BookmarkEvent -> handleBookmarkEvent(event) + is PinEvent -> handlePinEvent(event) + is BlockEvent -> removeAllByAccountId(event.accountId) + is StatusComposedEvent -> handleStatusComposedEvent(event) + is StatusDeletedEvent -> handleStatusDeletedEvent(event) + } + } + } + + loadFilters() + } + + fun loadThread(id: String) { + viewModelScope.launch { + val contextCall = async { api.statusContext(id) } + val statusCall = async { api.statusAsync(id) } + + val contextResult = contextCall.await() + val statusResult = statusCall.await() + + val status = statusResult.getOrElse { exception -> + _uiState.value = ThreadUiState.Error(exception) + return@launch + } + + contextResult.fold({ statusContext -> + + val ancestors = statusContext.ancestors.map { status -> status.toViewData() }.filter() + val detailedStatus = status.toViewData(true) + val descendants = statusContext.descendants.map { status -> status.toViewData() }.filter() + val statuses = ancestors + detailedStatus + descendants + + _uiState.value = ThreadUiState.Success( + statuses = statuses, + revealButton = statuses.getRevealButtonState(), + refreshing = false + ) + }, { throwable -> + _errors.emit(throwable) + _uiState.value = ThreadUiState.Success( + statuses = listOf(status.toViewData(true)), + revealButton = RevealButtonState.NO_BUTTON, + refreshing = false + ) + }) + } + } + + fun retry(id: String) { + _uiState.value = ThreadUiState.Loading + loadThread(id) + } + + fun refresh(id: String) { + updateSuccess { uiState -> + uiState.copy(refreshing = true) + } + loadThread(id) + } + + fun detailedStatus(): StatusViewData.Concrete? { + return (_uiState.value as ThreadUiState.Success?)?.statuses?.find { status -> + status.isDetailed + } + } + + fun reblog(reblog: Boolean, status: StatusViewData.Concrete): Job = viewModelScope.launch { + try { + timelineCases.reblog(status.actionableId, reblog).await() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to reblog status " + status.actionableId, t) + } + } + } + + fun favorite(favorite: Boolean, status: StatusViewData.Concrete): Job = viewModelScope.launch { + try { + timelineCases.favourite(status.actionableId, favorite).await() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to favourite status " + status.actionableId, t) + } + } + } + + fun bookmark(bookmark: Boolean, status: StatusViewData.Concrete): Job = viewModelScope.launch { + try { + timelineCases.bookmark(status.actionableId, bookmark).await() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to favourite status " + status.actionableId, t) + } + } + } + + fun voteInPoll(choices: List, status: StatusViewData.Concrete): Job = viewModelScope.launch { + val poll = status.status.actionableStatus.poll ?: run { + Log.w(TAG, "No poll on status ${status.id}") + return@launch + } + + val votedPoll = poll.votedCopy(choices) + updateStatus(status.id) { status -> + status.copy(poll = votedPoll) + } + + try { + timelineCases.voteInPoll(status.actionableId, poll.id, choices).await() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to vote in poll: " + status.actionableId, t) + } + } + } + + fun removeStatus(statusToRemove: StatusViewData.Concrete) { + updateSuccess { uiState -> + uiState.copy( + statuses = uiState.statuses.filterNot { status -> status == statusToRemove } + ) + } + } + + fun changeExpanded(expanded: Boolean, status: StatusViewData.Concrete) { + updateSuccess { uiState -> + val statuses = uiState.statuses.map { viewData -> + if (viewData.id == status.id) { + viewData.copy(isExpanded = expanded) + } else { + viewData + } + } + uiState.copy( + statuses = statuses, + revealButton = statuses.getRevealButtonState() + ) + } + } + + fun changeContentShowing(isShowing: Boolean, status: StatusViewData.Concrete) { + updateStatusViewData(status.id) { viewData -> + viewData.copy(isShowingContent = isShowing) + } + } + + fun changeContentCollapsed(isCollapsed: Boolean, status: StatusViewData.Concrete) { + updateStatusViewData(status.id) { viewData -> + viewData.copy(isCollapsed = isCollapsed) + } + } + + private fun handleFavEvent(event: FavoriteEvent) { + updateStatus(event.statusId) { status -> + status.copy(favourited = event.favourite) + } + } + + private fun handleReblogEvent(event: ReblogEvent) { + updateStatus(event.statusId) { status -> + status.copy(reblogged = event.reblog) + } + } + + private fun handleBookmarkEvent(event: BookmarkEvent) { + updateStatus(event.statusId) { status -> + status.copy(bookmarked = event.bookmark) + } + } + + private fun handlePinEvent(event: PinEvent) { + updateStatus(event.statusId) { status -> + status.copy(pinned = event.pinned) + } + } + + private fun removeAllByAccountId(accountId: String) { + updateSuccess { uiState -> + uiState.copy( + statuses = uiState.statuses.filter { viewData -> + viewData.status.account.id == accountId + } + ) + } + } + + private fun handleStatusComposedEvent(event: StatusComposedEvent) { + val eventStatus = event.status + updateSuccess { uiState -> + val statuses = uiState.statuses + val detailedIndex = statuses.indexOfFirst { status -> status.isDetailed } + val repliedIndex = statuses.indexOfFirst { status -> eventStatus.inReplyToId == status.id } + if (detailedIndex != -1 && repliedIndex >= detailedIndex) { + // there is a new reply to the detailed status or below -> display it + val newStatuses = statuses.subList(0, repliedIndex + 1) + + eventStatus.toViewData() + + statuses.subList(repliedIndex + 1, statuses.size) + uiState.copy(statuses = newStatuses) + } else { + uiState + } + } + } + + private fun handleStatusDeletedEvent(event: StatusDeletedEvent) { + updateSuccess { uiState -> + uiState.copy( + statuses = uiState.statuses.filter { status -> + status.id != event.statusId + } + ) + } + } + + fun toggleRevealButton() { + updateSuccess { uiState -> + when (uiState.revealButton) { + RevealButtonState.HIDE -> uiState.copy( + statuses = uiState.statuses.map { viewData -> + viewData.copy(isExpanded = false) + }, + revealButton = RevealButtonState.REVEAL + ) + RevealButtonState.REVEAL -> uiState.copy( + statuses = uiState.statuses.map { viewData -> + viewData.copy(isExpanded = true) + }, + revealButton = RevealButtonState.HIDE + ) + else -> uiState + } + } + } + + private fun List.getRevealButtonState(): RevealButtonState { + val hasWarnings = any { viewData -> + viewData.status.spoilerText.isNotEmpty() + } + + return if (hasWarnings) { + val allExpanded = none { viewData -> + !viewData.isExpanded + } + if (allExpanded) { + RevealButtonState.HIDE + } else { + RevealButtonState.REVEAL + } + } else { + RevealButtonState.NO_BUTTON + } + } + + private fun loadFilters() { + viewModelScope.launch { + val filters = try { + api.getFilters().await() + } catch (t: Exception) { + Log.w(TAG, "Failed to fetch filters", t) + return@launch + } + filterModel.initWithFilters( + filters.filter { filter -> + filter.context.contains(Filter.THREAD) + } + ) + + updateSuccess { uiState -> + val statuses = uiState.statuses.filter() + uiState.copy( + statuses = statuses, + revealButton = statuses.getRevealButtonState() + ) + } + } + } + + private fun List.filter(): List { + return filter { status -> + status.isDetailed || !filterModel.shouldFilterStatus(status.status) + } + } + + private fun Status.toViewData(detailed: Boolean = false): StatusViewData.Concrete { + return toViewData( + isShowingContent = alwaysShowSensitiveMedia || !actionableStatus.sensitive, + isExpanded = alwaysOpenSpoiler, + isCollapsed = !detailed, + isDetailed = detailed + ) + } + + private inline fun updateSuccess(updater: (ThreadUiState.Success) -> ThreadUiState.Success) { + _uiState.update { uiState -> + if (uiState is ThreadUiState.Success) { + updater(uiState) + } else { + uiState + } + } + } + + private fun updateStatusViewData(statusId: String, updater: (StatusViewData.Concrete) -> StatusViewData.Concrete) { + updateSuccess { uiState -> + uiState.copy( + statuses = uiState.statuses.map { viewData -> + if (viewData.id == statusId) { + updater(viewData) + } else { + viewData + } + } + ) + } + } + + private fun updateStatus(statusId: String, updater: (Status) -> Status) { + updateStatusViewData(statusId) { viewData -> + viewData.copy( + status = updater(viewData.status) + ) + } + } + + companion object { + private const val TAG = "ViewThreadViewModel" + } +} + +sealed interface ThreadUiState { + object Loading : ThreadUiState + class Error(val throwable: Throwable) : ThreadUiState + data class Success( + val statuses: List, + val revealButton: RevealButtonState, + val refreshing: Boolean + ) : ThreadUiState +} + +enum class RevealButtonState { + NO_BUTTON, REVEAL, HIDE +} 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 d85f6c452..15d6d9cd2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ActivitiesModule.kt @@ -27,7 +27,6 @@ import com.keylesspalace.tusky.SplashActivity import com.keylesspalace.tusky.StatusListActivity import com.keylesspalace.tusky.TabPreferenceActivity import com.keylesspalace.tusky.ViewMediaActivity -import com.keylesspalace.tusky.ViewThreadActivity import com.keylesspalace.tusky.components.account.AccountActivity import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity import com.keylesspalace.tusky.components.compose.ComposeActivity @@ -39,6 +38,7 @@ import com.keylesspalace.tusky.components.preference.PreferencesActivity import com.keylesspalace.tusky.components.report.ReportActivity import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity import com.keylesspalace.tusky.components.search.SearchActivity +import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity import dagger.Module import dagger.android.ContributesAndroidInjector 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 b3c5eaf83..989fb526a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/FragmentBuildersModule.kt @@ -29,9 +29,9 @@ import com.keylesspalace.tusky.components.search.fragments.SearchAccountsFragmen import com.keylesspalace.tusky.components.search.fragments.SearchHashtagsFragment import com.keylesspalace.tusky.components.search.fragments.SearchStatusesFragment import com.keylesspalace.tusky.components.timeline.TimelineFragment +import com.keylesspalace.tusky.components.viewthread.ViewThreadFragment import com.keylesspalace.tusky.fragment.AccountListFragment import com.keylesspalace.tusky.fragment.NotificationsFragment -import com.keylesspalace.tusky.fragment.ViewThreadFragment import dagger.Module import dagger.android.ContributesAndroidInjector 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 c8f746e0a..05444b5ab 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt @@ -14,6 +14,7 @@ import com.keylesspalace.tusky.components.scheduled.ScheduledStatusViewModel import com.keylesspalace.tusky.components.search.SearchViewModel import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewModel import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel +import com.keylesspalace.tusky.components.viewthread.ViewThreadViewModel import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel import com.keylesspalace.tusky.viewmodel.EditProfileViewModel import com.keylesspalace.tusky.viewmodel.ListsViewModel @@ -108,5 +109,9 @@ abstract class ViewModelModule { @ViewModelKey(NetworkTimelineViewModel::class) internal abstract fun networkTimelineViewModel(viewModel: NetworkTimelineViewModel): ViewModel + @Binds + @IntoMap + @ViewModelKey(ViewThreadViewModel::class) + internal abstract fun viewThreadViewModel(viewModel: ViewThreadViewModel): ViewModel // Add more ViewModels here } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java deleted file mode 100644 index 1d6525d18..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ /dev/null @@ -1,683 +0,0 @@ -/* Copyright 2017 Andrew Dawson - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.fragment; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.text.TextUtils; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.arch.core.util.Function; -import androidx.lifecycle.Lifecycle; -import androidx.preference.PreferenceManager; -import androidx.recyclerview.widget.DividerItemDecoration; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.SimpleItemAnimator; -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; - -import com.google.android.material.snackbar.Snackbar; -import com.keylesspalace.tusky.AccountListActivity; -import com.keylesspalace.tusky.BaseActivity; -import com.keylesspalace.tusky.BuildConfig; -import com.keylesspalace.tusky.R; -import com.keylesspalace.tusky.ViewThreadActivity; -import com.keylesspalace.tusky.adapter.ThreadAdapter; -import com.keylesspalace.tusky.appstore.BlockEvent; -import com.keylesspalace.tusky.appstore.BookmarkEvent; -import com.keylesspalace.tusky.appstore.EventHub; -import com.keylesspalace.tusky.appstore.FavoriteEvent; -import com.keylesspalace.tusky.appstore.PinEvent; -import com.keylesspalace.tusky.appstore.ReblogEvent; -import com.keylesspalace.tusky.appstore.StatusComposedEvent; -import com.keylesspalace.tusky.appstore.StatusDeletedEvent; -import com.keylesspalace.tusky.di.Injectable; -import com.keylesspalace.tusky.entity.Filter; -import com.keylesspalace.tusky.entity.Poll; -import com.keylesspalace.tusky.entity.Status; -import com.keylesspalace.tusky.interfaces.StatusActionListener; -import com.keylesspalace.tusky.network.FilterModel; -import com.keylesspalace.tusky.network.MastodonApi; -import com.keylesspalace.tusky.settings.PrefKeys; -import com.keylesspalace.tusky.util.CardViewMode; -import com.keylesspalace.tusky.util.LinkHelper; -import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate; -import com.keylesspalace.tusky.util.PairedList; -import com.keylesspalace.tusky.util.StatusDisplayOptions; -import com.keylesspalace.tusky.util.ViewDataUtils; -import com.keylesspalace.tusky.view.ConversationLineItemDecoration; -import com.keylesspalace.tusky.viewdata.AttachmentViewData; -import com.keylesspalace.tusky.viewdata.StatusViewData; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; - -import javax.inject.Inject; - -import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider; -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import kotlin.collections.CollectionsKt; - -import static autodispose2.AutoDispose.autoDisposable; -import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from; - -public final class ViewThreadFragment extends SFragment implements - SwipeRefreshLayout.OnRefreshListener, StatusActionListener, Injectable { - private static final String TAG = "ViewThreadFragment"; - - @Inject - public MastodonApi mastodonApi; - @Inject - public EventHub eventHub; - @Inject - public FilterModel filterModel; - - private SwipeRefreshLayout swipeRefreshLayout; - private RecyclerView recyclerView; - private ThreadAdapter adapter; - private String thisThreadsStatusId; - private boolean alwaysShowSensitiveMedia; - private boolean alwaysOpenSpoiler; - - private int statusIndex = 0; - - private final PairedList statuses = - new PairedList<>(new Function() { - @Override - public StatusViewData.Concrete apply(Status status) { - return ViewDataUtils.statusToViewData( - status, - alwaysShowSensitiveMedia || !status.getActionableStatus().getSensitive(), - alwaysOpenSpoiler, - true - ); - } - }); - - public static ViewThreadFragment newInstance(String id) { - Bundle arguments = new Bundle(1); - ViewThreadFragment fragment = new ViewThreadFragment(); - arguments.putString("id", id); - fragment.setArguments(arguments); - return fragment; - } - - @Override - public void onCreate(@Nullable Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - thisThreadsStatusId = getArguments().getString("id"); - SharedPreferences preferences = - PreferenceManager.getDefaultSharedPreferences(getActivity()); - - StatusDisplayOptions statusDisplayOptions = new StatusDisplayOptions( - preferences.getBoolean("animateGifAvatars", false), - accountManager.getActiveAccount().getMediaPreviewEnabled(), - preferences.getBoolean("absoluteTimeView", false), - preferences.getBoolean("showBotOverlay", true), - preferences.getBoolean("useBlurhash", true), - preferences.getBoolean("showCardsInTimelines", false) ? - CardViewMode.INDENTED : - CardViewMode.NONE, - preferences.getBoolean("confirmReblogs", true), - preferences.getBoolean("confirmFavourites", false), - preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), - preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) - ); - adapter = new ThreadAdapter(statusDisplayOptions, this); - } - - @Override - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, - @Nullable Bundle savedInstanceState) { - View rootView = inflater.inflate(R.layout.fragment_view_thread, container, false); - - Context context = getContext(); - swipeRefreshLayout = rootView.findViewById(R.id.swipeRefreshLayout); - swipeRefreshLayout.setOnRefreshListener(this); - swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue); - - recyclerView = rootView.findViewById(R.id.recyclerView); - recyclerView.setHasFixedSize(true); - LinearLayoutManager layoutManager = new LinearLayoutManager(context); - recyclerView.setLayoutManager(layoutManager); - recyclerView.setAccessibilityDelegateCompat( - new ListStatusAccessibilityDelegate(recyclerView, this, statuses::getPairedItemOrNull)); - DividerItemDecoration divider = new DividerItemDecoration( - context, layoutManager.getOrientation()); - recyclerView.addItemDecoration(divider); - - recyclerView.addItemDecoration(new ConversationLineItemDecoration(context)); - alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia(); - alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler(); - reloadFilters(); - - recyclerView.setAdapter(adapter); - - statuses.clear(); - - ((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false); - - return rootView; - } - - - @Override - public void onActivityCreated(@Nullable Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - onRefresh(); - - eventHub.getEvents() - .observeOn(AndroidSchedulers.mainThread()) - .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe(event -> { - if (event instanceof FavoriteEvent) { - handleFavEvent((FavoriteEvent) event); - } else if (event instanceof ReblogEvent) { - handleReblogEvent((ReblogEvent) event); - } else if (event instanceof BookmarkEvent) { - handleBookmarkEvent((BookmarkEvent) event); - } else if (event instanceof PinEvent) { - handlePinEvent(((PinEvent) event)); - } else if (event instanceof BlockEvent) { - removeAllByAccountId(((BlockEvent) event).getAccountId()); - } else if (event instanceof StatusComposedEvent) { - handleStatusComposedEvent((StatusComposedEvent) event); - } else if (event instanceof StatusDeletedEvent) { - handleStatusDeletedEvent((StatusDeletedEvent) event); - } - }); - } - - public void onRevealPressed() { - boolean allExpanded = allExpanded(); - for (int i = 0; i < statuses.size(); i++) { - updateViewData(i, statuses.getPairedItem(i).copyWithExpanded(!allExpanded)); - } - updateRevealIcon(); - } - - private boolean allExpanded() { - boolean allExpanded = true; - for (int i = 0; i < statuses.size(); i++) { - if (!statuses.getPairedItem(i).isExpanded()) { - allExpanded = false; - break; - } - } - return allExpanded; - } - - @Override - public void onRefresh() { - sendStatusRequest(thisThreadsStatusId); - sendThreadRequest(thisThreadsStatusId); - } - - @Override - public void onReply(int position) { - super.reply(statuses.get(position)); - } - - @Override - public void onReblog(final boolean reblog, final int position) { - final Status status = statuses.get(position); - - timelineCases.reblog(statuses.get(position).getId(), reblog) - .observeOn(AndroidSchedulers.mainThread()) - .to(autoDisposable(from(this))) - .subscribe( - this::replaceStatus, - (t) -> Log.d(TAG, - "Failed to reblog status: " + status.getId(), t) - ); - } - - @Override - public void onFavourite(final boolean favourite, final int position) { - final Status status = statuses.get(position); - - timelineCases.favourite(statuses.get(position).getId(), favourite) - .observeOn(AndroidSchedulers.mainThread()) - .to(autoDisposable(from(this))) - .subscribe( - this::replaceStatus, - (t) -> Log.d(TAG, - "Failed to favourite status: " + status.getId(), t) - ); - } - - @Override - public void onBookmark(final boolean bookmark, final int position) { - final Status status = statuses.get(position); - - timelineCases.bookmark(statuses.get(position).getId(), bookmark) - .observeOn(AndroidSchedulers.mainThread()) - .to(autoDisposable(from(this))) - .subscribe( - this::replaceStatus, - (t) -> Log.d(TAG, - "Failed to bookmark status: " + status.getId(), t) - ); - } - - private void replaceStatus(Status status) { - updateStatus(status.getId(), (__) -> status); - } - - private void updateStatus(String statusId, Function mapper) { - int position = indexOfStatus(statusId); - - if (position >= 0 && position < statuses.size()) { - Status oldStatus = statuses.get(position); - Status newStatus = mapper.apply(oldStatus); - StatusViewData.Concrete oldViewData = statuses.getPairedItem(position); - statuses.set(position, newStatus); - updateViewData(position, oldViewData.copyWithStatus(newStatus)); - } - } - - @Override - public void onMore(@NonNull View view, int position) { - super.more(statuses.get(position), view, position); - } - - @Override - public void onViewMedia(int position, int attachmentIndex, @NonNull View view) { - Status status = statuses.get(position); - super.viewMedia(attachmentIndex, AttachmentViewData.list(status), view); - } - - @Override - public void onViewThread(int position) { - Status status = statuses.get(position); - if (thisThreadsStatusId.equals(status.getId())) { - // If already viewing this thread, don't reopen it. - return; - } - super.viewThread(status.getActionableId(), status.getActionableStatus().getUrl()); - } - - @Override - public void onViewUrl(String url) { - Status status = null; - if (!statuses.isEmpty()) { - status = statuses.get(statusIndex); - } - if (status != null && status.getUrl().equals(url)) { - // already viewing the status with this url - // probably just a preview federated and the user is clicking again to view more -> open the browser - // this can happen with some friendica statuses - LinkHelper.openLink(requireContext(), url); - return; - } - super.onViewUrl(url); - } - - @Override - public void onOpenReblog(int position) { - // there should be no reblogs in the thread but let's implement it to be sure - super.openReblog(statuses.get(position)); - } - - @Override - public void onExpandedChange(boolean expanded, int position) { - updateViewData( - position, - statuses.getPairedItem(position).copyWithExpanded(expanded) - ); - updateRevealIcon(); - } - - @Override - public void onContentHiddenChange(boolean isShowing, int position) { - updateViewData( - position, - statuses.getPairedItem(position).copyWithShowingContent(isShowing) - ); - } - - private void updateViewData(int position, StatusViewData.Concrete newViewData) { - statuses.setPairedItem(position, newViewData); - adapter.setItem(position, newViewData, true); - } - - @Override - public void onLoadMore(int position) { - - } - - @Override - public void onShowReblogs(int position) { - String statusId = statuses.get(position).getId(); - Intent intent = AccountListActivity.newIntent(getContext(), AccountListActivity.Type.REBLOGGED, statusId); - ((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent); - } - - @Override - public void onShowFavs(int position) { - String statusId = statuses.get(position).getId(); - Intent intent = AccountListActivity.newIntent(getContext(), AccountListActivity.Type.FAVOURITED, statusId); - ((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent); - } - - @Override - public void onContentCollapsedChange(boolean isCollapsed, int position) { - adapter.setItem( - position, - statuses.getPairedItem(position).copyWithCollapsed(isCollapsed), - true - ); - } - - @Override - public void onViewTag(String tag) { - super.viewTag(tag); - } - - @Override - public void onViewAccount(String id) { - super.viewAccount(id); - } - - @Override - public void removeItem(int position) { - if (position == statusIndex) { - //the status got removed, close the activity - getActivity().finish(); - } - statuses.remove(position); - adapter.setStatuses(statuses.getPairedCopy()); - } - - public void onVoteInPoll(int position, @NonNull List choices) { - final Status status = statuses.get(position).getActionableStatus(); - - setVoteForPoll(status.getId(), status.getPoll().votedCopy(choices)); - - timelineCases.voteInPoll(status.getId(), status.getPoll().getId(), choices) - .observeOn(AndroidSchedulers.mainThread()) - .to(autoDisposable(from(this))) - .subscribe( - (newPoll) -> setVoteForPoll(status.getId(), newPoll), - (t) -> Log.d(TAG, - "Failed to vote in poll: " + status.getId(), t) - ); - - } - - private void setVoteForPoll(String statusId, Poll newPoll) { - updateStatus(statusId, s -> s.copyWithPoll(newPoll)); - } - - private void removeAllByAccountId(String accountId) { - Status status = null; - if (!statuses.isEmpty()) { - status = statuses.get(statusIndex); - } - // using iterator to safely remove items while iterating - Iterator iterator = statuses.iterator(); - while (iterator.hasNext()) { - Status s = iterator.next(); - if (s.getAccount().getId().equals(accountId) || s.getActionableStatus().getAccount().getId().equals(accountId)) { - iterator.remove(); - } - } - statusIndex = statuses.indexOf(status); - if (statusIndex == -1) { - //the status got removed, close the activity - getActivity().finish(); - return; - } - adapter.setDetailedStatusPosition(statusIndex); - adapter.setStatuses(statuses.getPairedCopy()); - } - - private void sendStatusRequest(final String id) { - mastodonApi.status(id) - .observeOn(AndroidSchedulers.mainThread()) - .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe( - status -> { - int position = setStatus(status); - recyclerView.scrollToPosition(position); - }, - throwable -> onThreadRequestFailure(id, throwable) - ); - } - - private void sendThreadRequest(final String id) { - mastodonApi.statusContext(id) - .observeOn(AndroidSchedulers.mainThread()) - .to(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) - .subscribe( - context -> { - swipeRefreshLayout.setRefreshing(false); - setContext(context.getAncestors(), context.getDescendants()); - }, - throwable -> onThreadRequestFailure(id, throwable) - ); - } - - private void onThreadRequestFailure(final String id, final Throwable throwable) { - View view = getView(); - swipeRefreshLayout.setRefreshing(false); - if (view != null) { - Snackbar.make(view, R.string.error_generic, Snackbar.LENGTH_LONG) - .setAction(R.string.action_retry, v -> { - sendThreadRequest(id); - sendStatusRequest(id); - }) - .show(); - } else { - Log.e(TAG, "Network request failed", throwable); - } - } - - private int setStatus(Status status) { - if (statuses.size() > 0 - && statusIndex < statuses.size() - && statuses.get(statusIndex).getId().equals(status.getId())) { - // Do not add this status on refresh, it's already in there. - statuses.set(statusIndex, status); - return statusIndex; - } - int i = statusIndex; - statuses.add(i, status); - adapter.setDetailedStatusPosition(i); - adapter.addItem(i, statuses.getPairedItem(i)); - updateRevealIcon(); - return i; - } - - private void setContext(List unfilteredAncestors, List unfilteredDescendants) { - Status mainStatus = null; - - // In case of refresh, remove old ancestors and descendants first. We'll remove all blindly, - // as we have no guarantee on their order to be the same as before - int oldSize = statuses.size(); - if (oldSize > 1) { - mainStatus = statuses.get(statusIndex); - statuses.clear(); - adapter.clearItems(); - } - - ArrayList ancestors = new ArrayList<>(); - for (Status status : unfilteredAncestors) - if (!filterModel.shouldFilterStatus(status)) - ancestors.add(status); - - // Insert newly fetched ancestors - statusIndex = ancestors.size(); - adapter.setDetailedStatusPosition(statusIndex); - statuses.addAll(0, ancestors); - List ancestorsViewDatas = statuses.getPairedCopy().subList(0, statusIndex); - if (BuildConfig.DEBUG && ancestors.size() != ancestorsViewDatas.size()) { - String error = String.format(Locale.getDefault(), - "Incorrectly got statusViewData sublist." + - " ancestors.size == %d ancestorsViewDatas.size == %d," + - " statuses.size == %d", - ancestors.size(), ancestorsViewDatas.size(), statuses.size()); - throw new AssertionError(error); - } - adapter.addAll(0, ancestorsViewDatas); - - if (mainStatus != null) { - // In case we needed to delete everything (which is way easier than deleting - // everything except one), re-insert the remaining status here. - // Not filtering the main status, since the user explicitly chose to be here - statuses.add(statusIndex, mainStatus); - StatusViewData.Concrete viewData = statuses.getPairedItem(statusIndex); - - adapter.addItem(statusIndex, viewData); - } - - ArrayList descendants = new ArrayList<>(); - for (Status status : unfilteredDescendants) - if (!filterModel.shouldFilterStatus(status)) - descendants.add(status); - - // Insert newly fetched descendants - statuses.addAll(descendants); - List descendantsViewData; - descendantsViewData = statuses.getPairedCopy() - .subList(statuses.size() - descendants.size(), statuses.size()); - if (BuildConfig.DEBUG && descendants.size() != descendantsViewData.size()) { - String error = String.format(Locale.getDefault(), - "Incorrectly got statusViewData sublist." + - " descendants.size == %d descendantsViewData.size == %d," + - " statuses.size == %d", - descendants.size(), descendantsViewData.size(), statuses.size()); - throw new AssertionError(error); - } - adapter.addAll(descendantsViewData); - updateRevealIcon(); - } - - private void handleFavEvent(FavoriteEvent event) { - updateStatus(event.getStatusId(), (s) -> { - s.setFavourited(event.getFavourite()); - return s; - }); - } - - private void handleReblogEvent(ReblogEvent event) { - updateStatus(event.getStatusId(), (s) -> { - s.setReblogged(event.getReblog()); - return s; - }); - } - - private void handleBookmarkEvent(BookmarkEvent event) { - updateStatus(event.getStatusId(), (s) -> { - s.setBookmarked(event.getBookmark()); - return s; - }); - } - - private void handlePinEvent(PinEvent event) { - updateStatus(event.getStatusId(), (s) -> s.copyWithPinned(event.getPinned())); - } - - - private void handleStatusComposedEvent(StatusComposedEvent event) { - Status eventStatus = event.getStatus(); - if (eventStatus.getInReplyToId() == null) return; - - if (eventStatus.getInReplyToId().equals(thisThreadsStatusId)) { - insertStatus(eventStatus, statuses.size()); - } else { - // If new status is a reply to some status in the thread, insert new status after it - // We only check statuses below main status, ones on top don't belong to this thread - for (int i = statusIndex; i < statuses.size(); i++) { - Status status = statuses.get(i); - if (eventStatus.getInReplyToId().equals(status.getId())) { - insertStatus(eventStatus, i + 1); - break; - } - } - } - } - - private void insertStatus(Status status, int at) { - statuses.add(at, status); - adapter.addItem(at, statuses.getPairedItem(at)); - } - - private void handleStatusDeletedEvent(StatusDeletedEvent event) { - int index = this.indexOfStatus(event.getStatusId()); - if (index != -1) { - statuses.remove(index); - adapter.removeItem(index); - } - } - - - private int indexOfStatus(String statusId) { - return CollectionsKt.indexOfFirst(this.statuses, (s) -> s.getId().equals(statusId)); - } - - private void updateRevealIcon() { - ViewThreadActivity activity = ((ViewThreadActivity) getActivity()); - if (activity == null) return; - - boolean hasAnyWarnings = false; - // Statuses are updated from the main thread so nothing should change while iterating - for (int i = 0; i < statuses.size(); i++) { - if (!TextUtils.isEmpty(statuses.get(i).getSpoilerText())) { - hasAnyWarnings = true; - break; - } - } - if (!hasAnyWarnings) { - activity.setRevealButtonState(ViewThreadActivity.REVEAL_BUTTON_HIDDEN); - return; - } - activity.setRevealButtonState(allExpanded() ? ViewThreadActivity.REVEAL_BUTTON_HIDE : - ViewThreadActivity.REVEAL_BUTTON_REVEAL); - } - - private void reloadFilters() { - mastodonApi.getFilters() - .to(autoDisposable(AndroidLifecycleScopeProvider.from(this))) - .subscribe( - (filters) -> { - List relevantFilters = CollectionsKt.filter( - filters, - (f) -> f.getContext().contains(Filter.THREAD) - ); - filterModel.initWithFilters(relevantFilters); - - recyclerView.post(this::applyFilters); - }, - (t) -> Log.e(TAG, "Failed to load filters", t) - ); - } - - private void applyFilters() { - CollectionsKt.removeAll(this.statuses, filterModel::shouldFilterStatus); - adapter.setStatuses(this.statuses.getPairedCopy()); - } -} 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 12d142ba5..e72800c4b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -167,10 +167,15 @@ interface MastodonApi { @Path("id") statusId: String ): Single - @GET("api/v1/statuses/{id}/context") - fun statusContext( + @GET("api/v1/statuses/{id}") + suspend fun statusAsync( @Path("id") statusId: String - ): Single + ): NetworkResult + + @GET("api/v1/statuses/{id}/context") + suspend fun statusContext( + @Path("id") statusId: String + ): NetworkResult @GET("api/v1/statuses/{id}/reblogged_by") fun statusRebloggedBy( 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 fef9c0bb8..3facc3a9a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt @@ -25,13 +25,15 @@ import com.keylesspalace.tusky.viewdata.StatusViewData fun Status.toViewData( isShowingContent: Boolean, isExpanded: Boolean, - isCollapsed: Boolean + isCollapsed: Boolean, + isDetailed: Boolean = false ): StatusViewData.Concrete { return StatusViewData.Concrete( status = this, isShowingContent = isShowingContent, isCollapsed = isCollapsed, isExpanded = isExpanded, + isDetailed = isDetailed ) } diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt index bef7d0e1d..ac9df9c27 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt @@ -42,6 +42,7 @@ sealed class StatusViewData { */ /** Whether the status meets the requirement to be collapse */ val isCollapsed: Boolean, + val isDetailed: Boolean = false ) : StatusViewData() { override val id: String get() = status.id diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml new file mode 100644 index 000000000..83808c544 --- /dev/null +++ b/app/src/main/res/drawable/ic_back.xml @@ -0,0 +1,12 @@ + + + + \ 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 150f0860c..a74cc83b7 100644 --- a/app/src/main/res/layout-sw640dp/fragment_view_thread.xml +++ b/app/src/main/res/layout-sw640dp/fragment_view_thread.xml @@ -1,15 +1,30 @@ - + android:layout_height="match_parent"> + + + + + + + - \ No newline at end of file + + + + + diff --git a/app/src/main/res/layout/activity_view_thread.xml b/app/src/main/res/layout/activity_view_thread.xml index 66b156dcc..c6a79182b 100644 --- a/app/src/main/res/layout/activity_view_thread.xml +++ b/app/src/main/res/layout/activity_view_thread.xml @@ -2,12 +2,9 @@ - - + tools:context="com.keylesspalace.tusky.components.viewthread.ViewThreadActivity"> - + android:layout_height="match_parent"> - + + + + + + + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" + android:layout_gravity="top"> - + + + + + + + + + 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 8781f6d9e..fc8e15b34 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 @@ -10,7 +10,15 @@ import java.util.Date private val fixedDate = Date(1638889052000) -fun mockStatus(id: String = "100") = Status( +fun mockStatus( + id: String = "100", + inReplyToId: String? = null, + inReplyToAccountId: String? = null, + spoilerText: String = "", + reblogged: Boolean = false, + favourited: Boolean = true, + bookmarked: Boolean = true +) = Status( id = id, url = "https://mastodon.example/@ConnyDuck/$id", account = TimelineAccount( @@ -21,8 +29,8 @@ fun mockStatus(id: String = "100") = Status( url = "https://mastodon.example/@ConnyDuck", avatar = "https://mastodon.example/system/accounts/avatars/000/150/486/original/ab27d7ddd18a10ea.jpg" ), - inReplyToId = null, - inReplyToAccountId = null, + inReplyToId = inReplyToId, + inReplyToAccountId = inReplyToAccountId, reblog = null, content = "Test", createdAt = fixedDate, @@ -30,11 +38,11 @@ fun mockStatus(id: String = "100") = Status( reblogsCount = 1, favouritesCount = 2, repliesCount = 3, - reblogged = false, - favourited = true, - bookmarked = true, + reblogged = reblogged, + favourited = favourited, + bookmarked = bookmarked, sensitive = true, - spoilerText = "", + spoilerText = spoilerText, visibility = Status.Visibility.PUBLIC, attachments = ArrayList(), mentions = emptyList(), @@ -46,11 +54,32 @@ fun mockStatus(id: String = "100") = Status( card = null ) -fun mockStatusViewData(id: String = "100") = StatusViewData.Concrete( - status = mockStatus(id), - isExpanded = false, - isShowingContent = false, - isCollapsed = true, +fun mockStatusViewData( + id: String = "100", + inReplyToId: String? = null, + inReplyToAccountId: String? = null, + isDetailed: Boolean = false, + spoilerText: String = "", + isExpanded: Boolean = false, + isShowingContent: Boolean = false, + isCollapsed: Boolean = !isDetailed, + reblogged: Boolean = false, + favourited: Boolean = true, + bookmarked: Boolean = true +) = StatusViewData.Concrete( + status = mockStatus( + id = id, + inReplyToId = inReplyToId, + inReplyToAccountId = inReplyToAccountId, + spoilerText = spoilerText, + reblogged = reblogged, + favourited = favourited, + bookmarked = bookmarked + ), + isExpanded = isExpanded, + isShowingContent = isShowingContent, + isCollapsed = isCollapsed, + isDetailed = isDetailed ) fun mockStatusEntityWithAccount( diff --git a/app/src/test/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModelTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModelTest.kt new file mode 100644 index 000000000..e1d690a14 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModelTest.kt @@ -0,0 +1,356 @@ +package com.keylesspalace.tusky.components.viewthread + +import android.os.Looper.getMainLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import at.connyduck.calladapter.networkresult.NetworkResult +import com.keylesspalace.tusky.appstore.BookmarkEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.FavoriteEvent +import com.keylesspalace.tusky.appstore.ReblogEvent +import com.keylesspalace.tusky.components.timeline.mockStatus +import com.keylesspalace.tusky.components.timeline.mockStatusViewData +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.StatusContext +import com.keylesspalace.tusky.network.FilterModel +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.usecase.TimelineCases +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config +import java.io.IOException + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class ViewThreadViewModelTest { + + private lateinit var api: MastodonApi + private lateinit var eventHub: EventHub + private lateinit var viewModel: ViewThreadViewModel + + private val threadId = "1234" + + @Before + fun setup() { + shadowOf(getMainLooper()).idle() + + api = mock() + eventHub = EventHub() + val filterModel = FilterModel() + val timelineCases = TimelineCases(api, eventHub) + val accountManager: AccountManager = mock { + on { activeAccount } doReturn AccountEntity( + id = 1, + domain = "mastodon.test", + accessToken = "fakeToken", + clientId = "fakeId", + clientSecret = "fakeSecret", + isActive = true + ) + } + viewModel = ViewThreadViewModel(api, filterModel, timelineCases, eventHub, accountManager) + } + + @Test + fun `should emit status and context when both load`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + + runBlocking { + assertEquals( + ThreadUiState.Success( + statuses = listOf( + mockStatusViewData(id = "1", spoilerText = "Test"), + mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test"), + mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test") + ), + revealButton = RevealButtonState.REVEAL, + refreshing = false + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should emit status even if context fails to load`() { + api.stub { + onBlocking { statusAsync(threadId) } doReturn NetworkResult.success(mockStatus(id = "2", inReplyToId = "1", inReplyToAccountId = "1")) + onBlocking { statusContext(threadId) } doReturn NetworkResult.failure(IOException()) + } + + viewModel.loadThread(threadId) + + runBlocking { + assertEquals( + ThreadUiState.Success( + statuses = listOf( + mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true) + ), + revealButton = RevealButtonState.NO_BUTTON, + refreshing = false + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should emit error when status and context fail to load`() { + api.stub { + onBlocking { statusAsync(threadId) } doReturn NetworkResult.failure(IOException()) + onBlocking { statusContext(threadId) } doReturn NetworkResult.failure(IOException()) + } + + viewModel.loadThread(threadId) + + runBlocking { + assertEquals( + ThreadUiState.Error::class.java, + viewModel.uiState.first().javaClass + ) + } + } + + @Test + fun `should emit error when status fails to load`() { + api.stub { + onBlocking { statusAsync(threadId) } doReturn NetworkResult.failure(IOException()) + onBlocking { statusContext(threadId) } doReturn NetworkResult.success( + StatusContext( + ancestors = listOf(mockStatus(id = "1")), + descendants = listOf(mockStatus(id = "3", inReplyToId = "2", inReplyToAccountId = "1")) + ) + ) + } + + viewModel.loadThread(threadId) + + runBlocking { + assertEquals( + ThreadUiState.Error::class.java, + viewModel.uiState.first().javaClass + ) + } + } + + @Test + fun `should update state when reveal button is toggled`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + viewModel.toggleRevealButton() + + runBlocking { + assertEquals( + ThreadUiState.Success( + statuses = listOf( + mockStatusViewData(id = "1", spoilerText = "Test", isExpanded = true), + mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isExpanded = true), + mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test", isExpanded = true) + ), + revealButton = RevealButtonState.HIDE, + refreshing = false + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should handle favorite event`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + + eventHub.dispatch(FavoriteEvent(statusId = "1", false)) + + runBlocking { + assertEquals( + ThreadUiState.Success( + statuses = listOf( + mockStatusViewData(id = "1", spoilerText = "Test", favourited = false), + mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test"), + mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test") + ), + revealButton = RevealButtonState.REVEAL, + refreshing = false + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should handle reblog event`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + + eventHub.dispatch(ReblogEvent(statusId = "2", true)) + + runBlocking { + assertEquals( + ThreadUiState.Success( + statuses = listOf( + mockStatusViewData(id = "1", spoilerText = "Test"), + mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", reblogged = true), + mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test") + ), + revealButton = RevealButtonState.REVEAL, + refreshing = false + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should handle bookmark event`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + + eventHub.dispatch(BookmarkEvent(statusId = "3", false)) + + runBlocking { + assertEquals( + ThreadUiState.Success( + statuses = listOf( + mockStatusViewData(id = "1", spoilerText = "Test"), + mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test"), + mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test", bookmarked = false) + ), + revealButton = RevealButtonState.REVEAL, + refreshing = false + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should remove status`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + + viewModel.removeStatus(mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")) + + runBlocking { + assertEquals( + ThreadUiState.Success( + statuses = listOf( + mockStatusViewData(id = "1", spoilerText = "Test"), + mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test") + ), + revealButton = RevealButtonState.REVEAL, + refreshing = false + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should change status expanded state`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + + viewModel.changeExpanded( + true, + mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test") + ) + + runBlocking { + assertEquals( + ThreadUiState.Success( + statuses = listOf( + mockStatusViewData(id = "1", spoilerText = "Test"), + mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isExpanded = true), + mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test") + ), + revealButton = RevealButtonState.REVEAL, + refreshing = false + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should change content collapsed state`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + + viewModel.changeContentCollapsed( + true, + mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test") + ) + + runBlocking { + assertEquals( + ThreadUiState.Success( + statuses = listOf( + mockStatusViewData(id = "1", spoilerText = "Test"), + mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isCollapsed = true), + mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test") + ), + revealButton = RevealButtonState.REVEAL, + refreshing = false + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should change content showing state`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + + viewModel.changeContentShowing( + true, + mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test") + ) + + runBlocking { + assertEquals( + ThreadUiState.Success( + statuses = listOf( + mockStatusViewData(id = "1", spoilerText = "Test"), + mockStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isShowingContent = true), + mockStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test") + ), + revealButton = RevealButtonState.REVEAL, + refreshing = false + ), + viewModel.uiState.first() + ) + } + } + + private fun mockSuccessResponses() { + api.stub { + onBlocking { statusAsync(threadId) } doReturn NetworkResult.success(mockStatus(id = "2", inReplyToId = "1", inReplyToAccountId = "1", spoilerText = "Test")) + onBlocking { statusContext(threadId) } doReturn NetworkResult.success( + StatusContext( + ancestors = listOf(mockStatus(id = "1", spoilerText = "Test")), + descendants = listOf(mockStatus(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")) + ) + ) + } + } +} From 2004d870a05ecc570c654f01e85766742e8f9bf3 Mon Sep 17 00:00:00 2001 From: Connyduck Date: Sat, 13 Aug 2022 15:27:11 +0000 Subject: [PATCH 035/142] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 99.5% (487 of 489 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 99.7% (488 of 489 strings) Co-authored-by: Connyduck Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nb_NO/ Translation: Tusky/Tusky --- app/src/main/res/values-nb-rNO/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 5ae17f236..fc990a221 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -537,4 +537,5 @@ Bildet kunne ikke redigeres. Lasting av kontodetaljer feilet Video- og lydfiler kan ikke være større enn %s MB. + \ No newline at end of file From 43aeea357df1ef4a6741072e17cf7680a486a255 Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Sat, 13 Aug 2022 15:27:11 +0000 Subject: [PATCH 036/142] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (489 of 489 strings) Co-authored-by: Vegard Skjefstad Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nb_NO/ Translation: Tusky/Tusky --- app/src/main/res/values-nb-rNO/strings.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index fc990a221..a75717e9b 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -537,5 +537,6 @@ 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 \ No newline at end of file From 77d3d1bb0fe5d2647596c4b0fe12258790f8e70b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Sat, 13 Aug 2022 15:27:11 +0000 Subject: [PATCH 037/142] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (489 of 489 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (489 of 489 strings) Co-authored-by: Hồ Nhất Duy Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ Translation: Tusky/Tusky --- app/src/main/res/values-vi/strings.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index bf4c575be..925476319 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -286,7 +286,7 @@ Thêm bộ lọc Chủ đề Thế giới - tiếp tục đọc + tải tút chưa đọc Trả lời @%s Media Luôn hiện nội dung bị ẩn @@ -526,4 +526,6 @@ Hình ảnh này không thể sửa. Không thể tải thông tin tài khoản Video và audio không thể quá %s MB. + Lỗi khi theo dõi #%s + Lỗi khi bỏ theo dõi #%s \ No newline at end of file From 0de452a3324f651f1bffe5b31633b3eb4548c5f7 Mon Sep 17 00:00:00 2001 From: GunChleoc Date: Sat, 13 Aug 2022 15:27:11 +0000 Subject: [PATCH 038/142] Translated using Weblate (Gaelic) Currently translated at 100.0% (489 of 489 strings) Co-authored-by: GunChleoc Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gd/ Translation: Tusky/Tusky --- app/src/main/res/values-gd/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index c5b76f0f2..2a0f9cea8 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -565,4 +565,6 @@ Mearachd a’ sgur de leantainn air #%s Mearachd a’ luchdadh fiosrachadh a’ chunntais Cha b’ urrainn dhuinn an dealbh a dheasachadh. + Airson brathan putaidh slighe UnifiedPush a chleachdadh, feumaidh Tusky cead airson fo-sgrìobhadh air brathan air an fhrithealaiche Mastodon agad fhaighinn. Bidh feum air clàradh a-steach às ùr airson na sgòpaichean OAuth a chaidh a cheadachadh dha Tusky atharrachadh. Ma nì thu clàradh a-steach às ùr an-seo no ann an roghainnean a’ chunntais, cumaidh sinn na dreachdan is an tasgadan ionadail agad. + Rinn thu clàradh a-steach às ùr dhan chunntas làithreach agad airson cead fo-sgrìobhadh putaidh a thoirt dha Tusky. Gidheadh, cha cunntasan eile agad fhathast nach deach imrich air an dòigh sin. Geàrr leum thuca is dèan clàradh a-steach às ùr do gach fear dhiubh airson taic do bhrathan UnifiedPush a chur an comas dhaibh. \ No newline at end of file From b54ee32ea1f65cde44eb9d2a89f140ad72536f5a Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Sat, 13 Aug 2022 15:27:11 +0000 Subject: [PATCH 039/142] Translated using Weblate (Ukrainian) Currently translated at 100.0% (489 of 489 strings) Co-authored-by: Ihor Hordiichuk Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translation: Tusky/Tusky --- app/src/main/res/values-uk/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 7bddf3412..9a902e064 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -558,4 +558,7 @@ 1+ Неможливо редагувати зображення. Не вдалося завантажити подробиці облікового запису + Помилка підписки на #%s + Розмір відео та аудіофайлів не може перевищувати %s Мб. + Помилка скасування підписки на #%s \ No newline at end of file From b21def504189aa72ad9122f2da6447148503c3b9 Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Mon, 15 Aug 2022 11:01:04 +0200 Subject: [PATCH 040/142] Respect filter expiration date when applying filters (#2661) * Respect filter expiration date when applying filters. #2578 * Fix typing for filter `expires_in` api points --- .../keylesspalace/tusky/FiltersActivity.kt | 4 +-- .../com/keylesspalace/tusky/entity/Filter.kt | 3 +- .../tusky/network/FilterModel.kt | 5 ++- .../tusky/network/MastodonApi.kt | 4 +-- .../com/keylesspalace/tusky/FilterTest.kt | 36 ++++++++++++++++++- 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt index d6de5d8ec..9f2763101 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt @@ -56,7 +56,7 @@ class FiltersActivity : BaseActivity() { } private fun updateFilter(filter: Filter, itemIndex: Int) { - api.updateFilter(filter.id, filter.phrase, filter.context, filter.irreversible, filter.wholeWord, filter.expiresAt) + api.updateFilter(filter.id, filter.phrase, filter.context, filter.irreversible, filter.wholeWord, null) .enqueue(object : Callback { override fun onFailure(call: Call, t: Throwable) { Toast.makeText(this@FiltersActivity, "Error updating filter '${filter.phrase}'", Toast.LENGTH_SHORT).show() @@ -102,7 +102,7 @@ class FiltersActivity : BaseActivity() { } private fun createFilter(phrase: String, wholeWord: Boolean) { - api.createFilter(phrase, listOf(context), false, wholeWord, "").enqueue(object : Callback { + api.createFilter(phrase, listOf(context), false, wholeWord, null).enqueue(object : Callback { override fun onResponse(call: Call, response: Response) { val filterResponse = response.body() if (response.isSuccessful && filterResponse != null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt index 34b80e83b..af51a04b9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt @@ -16,12 +16,13 @@ package com.keylesspalace.tusky.entity import com.google.gson.annotations.SerializedName +import java.util.Date data class Filter( val id: String, val phrase: String, val context: List, - @SerializedName("expires_at") val expiresAt: String?, + @SerializedName("expires_at") val expiresAt: Date?, val irreversible: Boolean, @SerializedName("whole_word") val wholeWord: Boolean ) { diff --git a/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt b/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt index 062191ad0..8adad95f6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt @@ -3,6 +3,7 @@ package com.keylesspalace.tusky.network import android.text.TextUtils import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Status +import java.util.Date import java.util.regex.Pattern import javax.inject.Inject @@ -54,7 +55,9 @@ class FilterModel @Inject constructor() { private fun makeFilter(filters: List): Pattern? { if (filters.isEmpty()) return null - val tokens = filters.map { filterToRegexToken(it) } + val tokens = filters + .filter { it.expiresAt?.before(Date()) != true } + .map { filterToRegexToken(it) } return Pattern.compile(TextUtils.join("|", tokens), Pattern.CASE_INSENSITIVE) } 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 e72800c4b..e37081c3d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -536,7 +536,7 @@ interface MastodonApi { @Field("context[]") context: List, @Field("irreversible") irreversible: Boolean?, @Field("whole_word") wholeWord: Boolean?, - @Field("expires_in") expiresIn: String? + @Field("expires_in") expiresIn: Int? ): Call @FormUrlEncoded @@ -547,7 +547,7 @@ interface MastodonApi { @Field("context[]") context: List, @Field("irreversible") irreversible: Boolean?, @Field("whole_word") wholeWord: Boolean?, - @Field("expires_in") expiresIn: String? + @Field("expires_in") expiresIn: Int? ): Call @DELETE("api/v1/filters/{id}") diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt index 521f01d69..bb2447dbc 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt @@ -14,6 +14,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock import org.robolectric.annotation.Config +import java.time.Instant import java.util.ArrayList import java.util.Date @@ -50,7 +51,23 @@ class FilterTest { expiresAt = null, irreversible = false, wholeWord = true - ) + ), + Filter( + id = "123", + phrase = "expired", + context = listOf(Filter.HOME), + expiresAt = Date.from(Instant.now().minusSeconds(10)), + irreversible = false, + wholeWord = true + ), + Filter( + id = "123", + phrase = "unexpired", + context = listOf(Filter.HOME), + expiresAt = Date.from(Instant.now().plusSeconds(3600)), + irreversible = false, + wholeWord = true + ), ) filterModel.initWithFilters(filters) @@ -148,6 +165,23 @@ class FilterTest { ) } + @Test + fun shouldNotFilter_whenFilterIsExpired() { + assertFalse( + filterModel.shouldFilterStatus( + mockStatus(content = "content matching expired filter should not be filtered") + ) + ) + } + + @Test + fun shouldFilter_whenFilterIsUnexpired() { + assertTrue( + filterModel.shouldFilterStatus( + mockStatus(content = "content matching unexpired filter should be filtered") + ) + ) + } private fun mockStatus( content: String = "", spoilerText: String = "", From decd8a0f4bb7e206e71a647dbc2bef70b2ad1ab7 Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Mon, 15 Aug 2022 11:01:17 +0200 Subject: [PATCH 041/142] Upgrade robolectric (#2664) * Upgrade robolectric to 4.8.1 * Make TimelineDAO cleanup test deterministic --- app/build.gradle | 2 +- .../test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d44a12f4b..bbcde3ab3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -185,7 +185,7 @@ dependencies { implementation "com.github.UnifiedPush:android-connector:2.0.0" testImplementation "androidx.test.ext:junit:1.1.3" - testImplementation "org.robolectric:robolectric:4.4" + testImplementation "org.robolectric:robolectric:4.8.1" testImplementation "org.mockito:mockito-inline:4.4.0" testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0" 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 620f73403..b629f9d86 100644 --- a/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt @@ -101,7 +101,7 @@ class TimelineDaoTest { assertStatuses(statusesAfterCleanup, loadedStatuses) val loadedAccounts: MutableList> = mutableListOf() - val accountCursor = db.query("SELECT timelineUserId, serverId FROM TimelineAccountEntity", null) + val accountCursor = db.query("SELECT timelineUserId, serverId FROM TimelineAccountEntity ORDER BY timelineUserId, serverId", null) accountCursor.moveToFirst() while (!accountCursor.isAfterLast) { val accountId: Long = accountCursor.getLong(accountCursor.getColumnIndex("timelineUserId")) @@ -111,10 +111,10 @@ class TimelineDaoTest { } val expectedAccounts = listOf( - 1L to "3", 1L to "10", - 1L to "R10", 1L to "20", + 1L to "3", + 1L to "R10", 2L to "5" ) From 30c87e04e2f1e10d92c95e0acfab12c7c08843c1 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 16 Aug 2022 20:06:00 +0200 Subject: [PATCH 042/142] upgrade dagger to 2.43.2 (#2665) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index bbcde3ab3..afde1eb5f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -98,7 +98,7 @@ ext.roomVersion = '2.4.3' ext.retrofitVersion = '2.9.0' ext.okhttpVersion = '4.9.3' ext.glideVersion = '4.13.1' -ext.daggerVersion = '2.42' +ext.daggerVersion = '2.43.2' ext.materialdrawerVersion = '8.4.5' ext.emoji2_version = '1.1.0' ext.filemojicompat_version = '3.2.3' From 3c7bfb70473fef1a8b1df8522013a82f7b8f12bc Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 16 Aug 2022 20:06:22 +0200 Subject: [PATCH 043/142] upgrade okhttp to 4.10.0 (#2666) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index afde1eb5f..e6e6d6318 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,7 +96,7 @@ ext.coroutinesVersion = "1.6.4" ext.lifecycleVersion = "2.5.1" ext.roomVersion = '2.4.3' ext.retrofitVersion = '2.9.0' -ext.okhttpVersion = '4.9.3' +ext.okhttpVersion = '4.10.0' ext.glideVersion = '4.13.1' ext.daggerVersion = '2.43.2' ext.materialdrawerVersion = '8.4.5' From 46278636f47aad878464f73a88a951f2af759300 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 16 Aug 2022 20:06:48 +0200 Subject: [PATCH 044/142] upgrade mockito-inline to 4.7.0 (#2668) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index e6e6d6318..f4265c645 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -186,7 +186,7 @@ dependencies { testImplementation "androidx.test.ext:junit:1.1.3" testImplementation "org.robolectric:robolectric:4.8.1" - testImplementation "org.mockito:mockito-inline:4.4.0" + testImplementation "org.mockito:mockito-inline:4.7.0" testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0" testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" From 94ae64b52d3454fc64dbfbe0c17d0f9e3f41993b Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 16 Aug 2022 20:06:59 +0200 Subject: [PATCH 045/142] upgrade android image cropper to 4.3.1 (#2669) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f4265c645..0389ed9d5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -175,7 +175,7 @@ dependencies { implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion" implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar' - implementation "com.github.CanHub:Android-Image-Cropper:4.2.1" + implementation "com.github.CanHub:Android-Image-Cropper:4.3.1" implementation "de.c1710:filemojicompat-ui:$filemojicompat_version" implementation "de.c1710:filemojicompat:$filemojicompat_version" From edbc624625178e2e6b1bd2219b564775cdbe2e84 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 16 Aug 2022 20:07:49 +0200 Subject: [PATCH 046/142] fix saving multiple attachments as draft (#2670) --- .../keylesspalace/tusky/components/drafts/DraftHelper.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt index 45da941d6..1c41132ea 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt @@ -78,11 +78,11 @@ class DraftHelper @Inject constructor( val uris = mediaUris.map { uriString -> uriString.toUri() - }.mapNotNull { uri -> + }.mapIndexedNotNull { index, uri -> if (uri.isInFolder(draftDirectory)) { uri } else { - uri.copyToFolder(draftDirectory) + uri.copyToFolder(draftDirectory, index) } } @@ -155,7 +155,7 @@ class DraftHelper @Inject constructor( return File(filePath).parentFile == folder } - private fun Uri.copyToFolder(folder: File): Uri? { + private fun Uri.copyToFolder(folder: File, index: Int): Uri? { val contentResolver = context.contentResolver val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) @@ -167,7 +167,7 @@ class DraftHelper @Inject constructor( map.getExtensionFromMimeType(mimeType) } - val filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension) + val filename = String.format("Tusky_Draft_Media_%s_%d.%s", timeStamp, index, fileExtension) val file = File(folder, filename) if (scheme == "https") { From 7a53bce439fc16ca5089dd689782a829940a9a83 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 16 Aug 2022 20:08:03 +0200 Subject: [PATCH 047/142] improve sending error notifications (#2671) * improve sending error notifications * rename draftNotification -> buildDraftNotification --- .../com/keylesspalace/tusky/MainActivity.kt | 7 ++ .../notifications/NotificationHelper.java | 2 - .../tusky/service/SendStatusService.kt | 68 +++++++++++++++---- 3 files changed, 61 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 4e1603fe0..8958feb01 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -175,6 +175,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje if (accountRequested && accountId != activeAccount.id) { accountManager.setActiveAccount(accountId) } + + val openDrafts = intent.getBooleanExtra(OPEN_DRAFTS, false) + if (canHandleMimeType(intent.type)) { // Sharing to Tusky from an external app if (accountRequested) { @@ -199,6 +202,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } ) } + } else if (openDrafts) { + val intent = DraftsActivity.newIntent(this) + startActivity(intent) } else if (accountRequested && savedInstanceState == null) { // user clicked a notification, show notification tab showNotificationTab = true @@ -839,6 +845,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13 private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14 const val REDIRECT_URL = "redirectUrl" + const val OPEN_DRAFTS = "draft" } } 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 bb1599fb6..620d49a9a 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 @@ -38,7 +38,6 @@ import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationManagerCompat; import androidx.core.app.RemoteInput; import androidx.core.app.TaskStackBuilder; -import androidx.core.content.ContextCompat; import androidx.work.Constraints; import androidx.work.NetworkType; import androidx.work.PeriodicWorkRequest; @@ -57,7 +56,6 @@ import com.keylesspalace.tusky.entity.Notification; import com.keylesspalace.tusky.entity.Poll; import com.keylesspalace.tusky.entity.PollOption; import com.keylesspalace.tusky.entity.Status; -import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.receiver.NotificationClearBroadcastReceiver; import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver; import com.keylesspalace.tusky.util.StringUtils; diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt index 13af53ec6..9471ee2fa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt @@ -1,5 +1,6 @@ package com.keylesspalace.tusky.service +import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.app.PendingIntent @@ -12,9 +13,11 @@ import android.os.Build import android.os.IBinder import android.os.Parcelable import android.util.Log +import androidx.annotation.StringRes import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat import at.connyduck.calladapter.networkresult.fold +import com.keylesspalace.tusky.MainActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.StatusComposedEvent @@ -184,14 +187,15 @@ class SendStatusService : Service(), Injectable { statusesToSend.remove(statusId) saveStatusToDrafts(statusToSend) - val builder = NotificationCompat.Builder(this@SendStatusService, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notify) - .setContentTitle(getString(R.string.send_post_notification_error_title)) - .setContentText(getString(R.string.send_post_notification_saved_content)) - .setColor(getColor(R.color.notification_color)) + val notification = buildDraftNotification( + R.string.send_post_notification_error_title, + R.string.send_post_notification_saved_content, + statusToSend.accountId, + statusId + ) notificationManager.cancel(statusId) - notificationManager.notify(errorNotificationId--, builder.build()) + notificationManager.notify(errorNotificationId--, notification) } else { // a network problem occurred, let's retry sending the status retrySending(statusId) @@ -227,15 +231,18 @@ class SendStatusService : Service(), Injectable { saveStatusToDrafts(statusToCancel) - val builder = NotificationCompat.Builder(this@SendStatusService, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_notify) - .setContentTitle(getString(R.string.send_post_notification_cancel_title)) - .setContentText(getString(R.string.send_post_notification_saved_content)) - .setColor(getColor(R.color.notification_color)) + val notification = buildDraftNotification( + R.string.send_post_notification_cancel_title, + R.string.send_post_notification_saved_content, + statusToCancel.accountId, + statusId + ) - notificationManager.notify(statusId, builder.build()) + notificationManager.notify(statusId, notification) delay(5000) + + stopSelfWhenDone() } } @@ -259,7 +266,41 @@ class SendStatusService : Service(), Injectable { private fun cancelSendingIntent(statusId: Int): PendingIntent { val intent = Intent(this, SendStatusService::class.java) intent.putExtra(KEY_CANCEL, statusId) - return PendingIntent.getService(this, statusId, intent, NotificationHelper.pendingIntentFlags(false)) + return PendingIntent.getService( + this, + statusId, + intent, + NotificationHelper.pendingIntentFlags(false) + ) + } + + private fun buildDraftNotification( + @StringRes title: Int, + @StringRes content: Int, + accountId: Long, + statusId: Int + ): Notification { + + val intent = Intent(this, MainActivity::class.java) + intent.putExtra(NotificationHelper.ACCOUNT_ID, accountId) + intent.putExtra(MainActivity.OPEN_DRAFTS, true) + + val pendingIntent = PendingIntent.getActivity( + this, + statusId, + intent, + NotificationHelper.pendingIntentFlags(false) + ) + + return NotificationCompat.Builder(this@SendStatusService, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notify) + .setContentTitle(getString(title)) + .setContentText(getString(content)) + .setColor(getColor(R.color.notification_color)) + .setAutoCancel(true) + .setOngoing(false) + .setContentIntent(pendingIntent) + .build() } override fun onDestroy() { @@ -279,7 +320,6 @@ class SendStatusService : Service(), Injectable { private var sendingNotificationId = -1 // use negative ids to not clash with other notis private var errorNotificationId = Int.MIN_VALUE // use even more negative ids to not clash with other notis - @JvmStatic fun sendStatusIntent( context: Context, statusToSend: StatusToSend From 19ef80432a3db45e5fac6527c4a2b74771fbb768 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 16 Aug 2022 20:08:23 +0200 Subject: [PATCH 048/142] upgrade unified push connector to 2.0.1 (#2672) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 0389ed9d5..d5a18f260 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -182,7 +182,7 @@ dependencies { implementation "de.c1710:filemojicompat-defaults:$filemojicompat_version" implementation "org.bouncycastle:bcprov-jdk15on:1.70" - implementation "com.github.UnifiedPush:android-connector:2.0.0" + implementation "com.github.UnifiedPush:android-connector:2.0.1" testImplementation "androidx.test.ext:junit:1.1.3" testImplementation "org.robolectric:robolectric:4.8.1" From 95191bbb8d8ffb767fdb31d85830949703ae06b0 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 16 Aug 2022 20:09:40 +0200 Subject: [PATCH 049/142] upgrade glide animation plugin to 2.23.0 (#2674) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index d5a18f260..659efeebc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -152,7 +152,7 @@ dependencies { implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion" kapt "com.github.bumptech.glide:compiler:$glideVersion" - implementation "com.github.penfeizhou.android.animation:glide-plugin:2.22.0" + implementation "com.github.penfeizhou.android.animation:glide-plugin:2.23.0" implementation "io.reactivex.rxjava3:rxjava:3.1.3" implementation "io.reactivex.rxjava3:rxandroid:3.0.0" From 9beea540ded39019ca5330fb9a95f33efb9ae140 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 16 Aug 2022 20:33:19 +0200 Subject: [PATCH 050/142] upgrade glide to 4.13.2 (#2673) --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 659efeebc..68d966b79 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,8 +96,8 @@ ext.coroutinesVersion = "1.6.4" ext.lifecycleVersion = "2.5.1" ext.roomVersion = '2.4.3' ext.retrofitVersion = '2.9.0' -ext.okhttpVersion = '4.10.0' -ext.glideVersion = '4.13.1' +ext.okhttpVersion = '4.9.3' +ext.glideVersion = '4.13.2' ext.daggerVersion = '2.43.2' ext.materialdrawerVersion = '8.4.5' ext.emoji2_version = '1.1.0' From c47d9ef6aca30ce63c3fd1f51e2eddaf4c3166bd Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Wed, 17 Aug 2022 17:50:34 +0200 Subject: [PATCH 051/142] Support setting filter expirations (#2667) * Show filter expiration in list * Add support for setting and updating the duration of a filter * Add tests for duration conversion math * Refactor network wrapper code * Mark updated mastodon api functions as suspend * Avoid creating unnecessary Date objects * Apply suggestions to filter dialog layout --- .../keylesspalace/tusky/FiltersActivity.kt | 146 ++++++++---------- .../tusky/network/MastodonApi.kt | 16 +- .../keylesspalace/tusky/view/FilterDialog.kt | 73 +++++++++ app/src/main/res/layout/dialog_filter.xml | 18 ++- app/src/main/res/values/donottranslate.xml | 24 ++- app/src/main/res/values/strings.xml | 2 + .../com/keylesspalace/tusky/FilterTest.kt | 18 +++ 7 files changed, 197 insertions(+), 100 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/view/FilterDialog.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt index 9f2763101..3a61df90f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/FiltersActivity.kt @@ -1,26 +1,25 @@ package com.keylesspalace.tusky import android.os.Bundle +import android.text.format.DateUtils import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.Toast -import androidx.appcompat.app.AlertDialog import androidx.lifecycle.lifecycleScope +import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.databinding.ActivityFiltersBinding -import com.keylesspalace.tusky.databinding.DialogFilterBinding import com.keylesspalace.tusky.entity.Filter 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.view.getSecondsForDurationIndex +import com.keylesspalace.tusky.view.setupEditDialogForFilter +import com.keylesspalace.tusky.view.showAddFilterDialog import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await -import okhttp3.ResponseBody -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response import java.io.IOException import javax.inject.Inject @@ -47,7 +46,7 @@ class FiltersActivity : BaseActivity() { setDisplayShowHomeEnabled(true) } binding.addFilterButton.setOnClickListener { - showAddFilterDialog() + showAddFilterDialog(this) } title = intent?.getStringExtra(FILTERS_TITLE) @@ -55,15 +54,10 @@ class FiltersActivity : BaseActivity() { loadFilters() } - private fun updateFilter(filter: Filter, itemIndex: Int) { - api.updateFilter(filter.id, filter.phrase, filter.context, filter.irreversible, filter.wholeWord, null) - .enqueue(object : Callback { - override fun onFailure(call: Call, t: Throwable) { - Toast.makeText(this@FiltersActivity, "Error updating filter '${filter.phrase}'", Toast.LENGTH_SHORT).show() - } - - override fun onResponse(call: Call, response: Response) { - val updatedFilter = response.body()!! + fun updateFilter(id: String, phrase: String, filterContext: List, irreversible: Boolean, wholeWord: Boolean, expiresInSeconds: Int?, itemIndex: Int) { + lifecycleScope.launch { + api.updateFilter(id, phrase, filterContext, irreversible, wholeWord, expiresInSeconds).fold( + { updatedFilter -> if (updatedFilter.context.contains(context)) { filters[itemIndex] = updatedFilter } else { @@ -71,25 +65,30 @@ class FiltersActivity : BaseActivity() { } refreshFilterDisplay() eventHub.dispatch(PreferenceChangedEvent(context)) + }, + { + Toast.makeText(this@FiltersActivity, "Error updating filter '$phrase'", Toast.LENGTH_SHORT).show() } - }) + ) + } } - private fun deleteFilter(itemIndex: Int) { + fun deleteFilter(itemIndex: Int) { val filter = filters[itemIndex] if (filter.context.size == 1) { - // This is the only context for this filter; delete it - api.deleteFilter(filters[itemIndex].id).enqueue(object : Callback { - override fun onFailure(call: Call, t: Throwable) { - Toast.makeText(this@FiltersActivity, "Error updating filter '${filters[itemIndex].phrase}'", Toast.LENGTH_SHORT).show() - } - - override fun onResponse(call: Call, response: Response) { - filters.removeAt(itemIndex) - refreshFilterDisplay() - eventHub.dispatch(PreferenceChangedEvent(context)) - } - }) + lifecycleScope.launch { + // This is the only context for this filter; delete it + api.deleteFilter(filters[itemIndex].id).fold( + { + filters.removeAt(itemIndex) + refreshFilterDisplay() + eventHub.dispatch(PreferenceChangedEvent(context)) + }, + { + Toast.makeText(this@FiltersActivity, "Error deleting filter '${filters[itemIndex].phrase}'", Toast.LENGTH_SHORT).show() + } + ) + } } else { // Keep the filter, but remove it from this context val oldFilter = filters[itemIndex] @@ -97,69 +96,50 @@ class FiltersActivity : BaseActivity() { oldFilter.id, oldFilter.phrase, oldFilter.context.filter { c -> c != context }, oldFilter.expiresAt, oldFilter.irreversible, oldFilter.wholeWord ) - updateFilter(newFilter, itemIndex) + updateFilter( + newFilter.id, newFilter.phrase, newFilter.context, newFilter.irreversible, newFilter.wholeWord, + getSecondsForDurationIndex(-1, this, oldFilter.expiresAt), itemIndex + ) } } - private fun createFilter(phrase: String, wholeWord: Boolean) { - api.createFilter(phrase, listOf(context), false, wholeWord, null).enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - val filterResponse = response.body() - if (response.isSuccessful && filterResponse != null) { - filters.add(filterResponse) + fun createFilter(phrase: String, wholeWord: Boolean, expiresInSeconds: Int? = null) { + lifecycleScope.launch { + api.createFilter(phrase, listOf(context), false, wholeWord, expiresInSeconds).fold( + { filter -> + filters.add(filter) refreshFilterDisplay() eventHub.dispatch(PreferenceChangedEvent(context)) - } else { + }, + { Toast.makeText(this@FiltersActivity, "Error creating filter '$phrase'", Toast.LENGTH_SHORT).show() } - } - - override fun onFailure(call: Call, t: Throwable) { - Toast.makeText(this@FiltersActivity, "Error creating filter '$phrase'", Toast.LENGTH_SHORT).show() - } - }) - } - - private fun showAddFilterDialog() { - val binding = DialogFilterBinding.inflate(layoutInflater) - binding.phraseWholeWord.isChecked = true - AlertDialog.Builder(this@FiltersActivity) - .setTitle(R.string.filter_addition_dialog_title) - .setView(binding.root) - .setPositiveButton(android.R.string.ok) { _, _ -> - createFilter(binding.phraseEditText.text.toString(), binding.phraseWholeWord.isChecked) - } - .setNeutralButton(android.R.string.cancel, null) - .show() - } - - private fun setupEditDialogForItem(itemIndex: Int) { - val binding = DialogFilterBinding.inflate(layoutInflater) - val filter = filters[itemIndex] - binding.phraseEditText.setText(filter.phrase) - binding.phraseWholeWord.isChecked = filter.wholeWord - - AlertDialog.Builder(this@FiltersActivity) - .setTitle(R.string.filter_edit_dialog_title) - .setView(binding.root) - .setPositiveButton(R.string.filter_dialog_update_button) { _, _ -> - val oldFilter = filters[itemIndex] - val newFilter = Filter( - oldFilter.id, binding.phraseEditText.text.toString(), oldFilter.context, - oldFilter.expiresAt, oldFilter.irreversible, binding.phraseWholeWord.isChecked - ) - updateFilter(newFilter, itemIndex) - } - .setNegativeButton(R.string.filter_dialog_remove_button) { _, _ -> - deleteFilter(itemIndex) - } - .setNeutralButton(android.R.string.cancel, null) - .show() + ) + } } private fun refreshFilterDisplay() { - binding.filtersView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, filters.map { filter -> filter.phrase }) - binding.filtersView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> setupEditDialogForItem(position) } + binding.filtersView.adapter = ArrayAdapter( + this, + android.R.layout.simple_list_item_1, + filters.map { filter -> + if (filter.expiresAt == null) { + filter.phrase + } else { + getString( + R.string.filter_expiration_format, + filter.phrase, + DateUtils.getRelativeTimeSpanString( + filter.expiresAt.time, + System.currentTimeMillis(), + DateUtils.MINUTE_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE + ) + ) + } + } + ) + binding.filtersView.onItemClickListener = AdapterView.OnItemClickListener { _, _, position, _ -> setupEditDialogForFilter(this, filters[position], position) } } private fun loadFilters() { 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 e37081c3d..cb07545cd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -531,29 +531,29 @@ interface MastodonApi { @FormUrlEncoded @POST("api/v1/filters") - fun createFilter( + suspend fun createFilter( @Field("phrase") phrase: String, @Field("context[]") context: List, @Field("irreversible") irreversible: Boolean?, @Field("whole_word") wholeWord: Boolean?, - @Field("expires_in") expiresIn: Int? - ): Call + @Field("expires_in") expiresInSeconds: Int? + ): NetworkResult @FormUrlEncoded @PUT("api/v1/filters/{id}") - fun updateFilter( + suspend fun updateFilter( @Path("id") id: String, @Field("phrase") phrase: String, @Field("context[]") context: List, @Field("irreversible") irreversible: Boolean?, @Field("whole_word") wholeWord: Boolean?, - @Field("expires_in") expiresIn: Int? - ): Call + @Field("expires_in") expiresInSeconds: Int? + ): NetworkResult @DELETE("api/v1/filters/{id}") - fun deleteFilter( + suspend fun deleteFilter( @Path("id") id: String - ): Call + ): NetworkResult @FormUrlEncoded @POST("api/v1/polls/{id}/votes") diff --git a/app/src/main/java/com/keylesspalace/tusky/view/FilterDialog.kt b/app/src/main/java/com/keylesspalace/tusky/view/FilterDialog.kt new file mode 100644 index 000000000..c6cea1e21 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/FilterDialog.kt @@ -0,0 +1,73 @@ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.widget.ArrayAdapter +import androidx.appcompat.app.AlertDialog +import com.keylesspalace.tusky.FiltersActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.DialogFilterBinding +import com.keylesspalace.tusky.entity.Filter +import java.util.Date + +fun showAddFilterDialog(activity: FiltersActivity) { + val binding = DialogFilterBinding.inflate(activity.layoutInflater) + binding.phraseWholeWord.isChecked = true + binding.filterDurationSpinner.adapter = ArrayAdapter( + activity, + android.R.layout.simple_list_item_1, + activity.resources.getStringArray(R.array.filter_duration_names) + ) + AlertDialog.Builder(activity) + .setTitle(R.string.filter_addition_dialog_title) + .setView(binding.root) + .setPositiveButton(android.R.string.ok) { _, _ -> + activity.createFilter( + binding.phraseEditText.text.toString(), binding.phraseWholeWord.isChecked, + getSecondsForDurationIndex(binding.filterDurationSpinner.selectedItemPosition, activity) + ) + } + .setNeutralButton(android.R.string.cancel, null) + .show() +} + +fun setupEditDialogForFilter(activity: FiltersActivity, filter: Filter, itemIndex: Int) { + val binding = DialogFilterBinding.inflate(activity.layoutInflater) + binding.phraseEditText.setText(filter.phrase) + binding.phraseWholeWord.isChecked = filter.wholeWord + val filterNames = activity.resources.getStringArray(R.array.filter_duration_names).toMutableList() + if (filter.expiresAt != null) { + filterNames.add(0, activity.getString(R.string.duration_no_change)) + } + binding.filterDurationSpinner.adapter = ArrayAdapter(activity, android.R.layout.simple_list_item_1, filterNames) + + AlertDialog.Builder(activity) + .setTitle(R.string.filter_edit_dialog_title) + .setView(binding.root) + .setPositiveButton(R.string.filter_dialog_update_button) { _, _ -> + var index = binding.filterDurationSpinner.selectedItemPosition + if (filter.expiresAt != null) { + // We prepended "No changes", account for that here + --index + } + activity.updateFilter( + filter.id, binding.phraseEditText.text.toString(), filter.context, + filter.irreversible, binding.phraseWholeWord.isChecked, + getSecondsForDurationIndex(index, activity, filter.expiresAt), itemIndex + ) + } + .setNegativeButton(R.string.filter_dialog_remove_button) { _, _ -> + activity.deleteFilter(itemIndex) + } + .setNeutralButton(android.R.string.cancel, null) + .show() +} + +// Mastodon *stores* the absolute date in the filter, +// but create/edit take a number of seconds (relative to the time the operation is posted) +fun getSecondsForDurationIndex(index: Int, context: Context?, default: Date? = null): Int? { + return when (index) { + -1 -> if (default == null) { default } else { ((default.time - System.currentTimeMillis()) / 1000).toInt() } + 0 -> null + else -> context?.resources?.getIntArray(R.array.filter_duration_values)?.get(index) + } +} diff --git a/app/src/main/res/layout/dialog_filter.xml b/app/src/main/res/layout/dialog_filter.xml index 09c6001dc..f331fed6b 100644 --- a/app/src/main/res/layout/dialog_filter.xml +++ b/app/src/main/res/layout/dialog_filter.xml @@ -4,32 +4,34 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingTop="16dp"> + android:padding="24dp"> + 31536000 + <b>%1$d%%</b> + @string/duration_indefinite @string/duration_5_min @@ -212,5 +214,25 @@ 604800 - <b>%1$d%%</b> + + @string/duration_indefinite + @string/duration_5_min + @string/duration_30_min + @string/duration_1_hour + @string/duration_6_hours + @string/duration_1_day + @string/duration_3_days + @string/duration_7_days + + + + 0 + 300 + 1800 + 3600 + 21600 + 86400 + 259200 + 604800 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 21b0708ef..72a1556cc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -383,6 +383,7 @@ Whole word When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word Phrase to filter + %s (%s) Add Account Add new Mastodon Account @@ -602,6 +603,7 @@ 90 days 180 days 365 days + (No change) Add choice Multiple choices Choice %d diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt index bb2447dbc..48139fc4e 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt @@ -7,6 +7,7 @@ import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.PollOption import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.FilterModel +import com.keylesspalace.tusky.view.getSecondsForDurationIndex import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before @@ -182,6 +183,23 @@ class FilterTest { ) ) } + + @Test + fun unchangedExpiration_shouldBeNegative_whenFilterIsExpired() { + val expiredBySeconds = 3600 + val expiredDate = Date.from(Instant.now().minusSeconds(expiredBySeconds.toLong())) + val updatedDuration = getSecondsForDurationIndex(-1, null, expiredDate) + assert(updatedDuration != null && updatedDuration <= -expiredBySeconds) + } + + @Test + fun unchangedExpiration_shouldBePositive_whenFilterIsUnexpired() { + val expiresInSeconds = 3600 + val expiredDate = Date.from(Instant.now().plusSeconds(expiresInSeconds.toLong())) + val updatedDuration = getSecondsForDurationIndex(-1, null, expiredDate) + assert(updatedDuration != null && updatedDuration > (expiresInSeconds - 60)) + } + private fun mockStatus( content: String = "", spoilerText: String = "", From e68e3d1825006c973f964ce7c878af04bcbfa8d8 Mon Sep 17 00:00:00 2001 From: Donno Date: Sat, 20 Aug 2022 16:26:34 +0200 Subject: [PATCH 052/142] a monochrome icon for tusky (#2683) --- app/src/main/res/drawable/ic_launcher_monochrome.xml | 12 ++++++++++++ app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml | 1 + 2 files changed, 13 insertions(+) create mode 100644 app/src/main/res/drawable/ic_launcher_monochrome.xml diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 000000000..53614b80d --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index fe5f12601..72f76c742 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -6,4 +6,5 @@ android:inset="10%" android:drawable="@drawable/ic_launcher_foreground" /> + \ No newline at end of file From f094f84b865d2ffdfd27f40a7c83912da007efce Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 22 Aug 2022 15:17:08 +0200 Subject: [PATCH 053/142] fix ConversationLineItemDecoration (#2677) * fix ConversationLineItemDecoration * cleanup code a bit --- .../viewthread/ConversationLineItemDecoration.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ConversationLineItemDecoration.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ConversationLineItemDecoration.kt index 7e98ebef7..36e36d10f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ConversationLineItemDecoration.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ConversationLineItemDecoration.kt @@ -20,6 +20,7 @@ import android.graphics.Canvas import android.graphics.drawable.Drawable import android.view.View import androidx.core.content.ContextCompat +import androidx.core.view.forEach import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R @@ -31,14 +32,13 @@ class ConversationLineItemDecoration(private val context: Context) : RecyclerVie val dividerStart = parent.paddingStart + context.resources.getDimensionPixelSize(R.dimen.status_line_margin_start) val dividerEnd = dividerStart + divider.intrinsicWidth - val childCount = parent.childCount val avatarMargin = context.resources.getDimensionPixelSize(R.dimen.account_avatar_margin) - for (i in 0 until childCount) { - val child = parent.getChildAt(i) + val items = (parent.adapter as ThreadAdapter).currentList + + parent.forEach { child -> val position = parent.getChildAdapterPosition(child) - val items = (parent.adapter as ThreadAdapter).currentList val current = items.getOrNull(position) @@ -50,7 +50,7 @@ class ConversationLineItemDecoration(private val context: Context) : RecyclerVie child.top + avatarMargin } val below = items.getOrNull(position + 1) - val dividerBottom = if (below != null && current.id == below.status.inReplyToId && below.isDetailed) { + val dividerBottom = if (below != null && current.id == below.status.inReplyToId && !current.isDetailed) { child.bottom } else { child.top + avatarMargin From e020b3577421ddf27c9e936f2a27c174f43569bd Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 22 Aug 2022 15:17:55 +0200 Subject: [PATCH 054/142] fix view thread back button content description (#2682) --- app/src/main/res/drawable/ic_back.xml | 12 ------------ .../main/res/layout-sw640dp/fragment_view_thread.xml | 9 +++++---- app/src/main/res/layout/fragment_view_thread.xml | 9 +++++---- 3 files changed, 10 insertions(+), 20 deletions(-) delete mode 100644 app/src/main/res/drawable/ic_back.xml diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml deleted file mode 100644 index 83808c544..000000000 --- a/app/src/main/res/drawable/ic_back.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - \ 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 a74cc83b7..ec023062a 100644 --- a/app/src/main/res/layout-sw640dp/fragment_view_thread.xml +++ b/app/src/main/res/layout-sw640dp/fragment_view_thread.xml @@ -8,15 +8,16 @@ android:id="@+id/appbar" android:layout_width="match_parent" android:layout_height="wrap_content" - android:elevation="@dimen/actionbar_elevation" > + android:elevation="@dimen/actionbar_elevation"> + app:navigationIcon="?attr/homeAsUpIndicator" + app:navigationContentDescription="@string/abc_action_bar_up_description" + app:menu="@menu/view_thread_toolbar" /> @@ -40,7 +41,7 @@ android:id="@+id/progressBar" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="center"/> + android:layout_gravity="center" /> + android:elevation="@dimen/actionbar_elevation"> + app:navigationIcon="?attr/homeAsUpIndicator" + app:navigationContentDescription="@string/abc_action_bar_up_description" + app:menu="@menu/view_thread_toolbar" /> @@ -40,7 +41,7 @@ android:id="@+id/progressBar" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="center"/> + android:layout_gravity="center" /> Date: Sat, 20 Aug 2022 14:26:38 +0000 Subject: [PATCH 055/142] Translated using Weblate (Esperanto) Currently translated at 96.9% (474 of 489 strings) Co-authored-by: Tobiasz Kubisiowski Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/eo/ Translation: Tusky/Tusky --- app/src/main/res/values-eo/strings.xml | 37 +++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 2db0ebb27..541759a1f 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -14,16 +14,16 @@ Tiu dosiero ne povas esti malfermita. Permeso legi aŭdovidaĵojn necesas. Permeso konservi aŭdovidaĵojn necesas. - Bildoj kaj videoj ne povas ambaŭ estas alligita al la sama mesaĝo. + Bildoj kaj videoj ne povas esti ambaŭ alkroĉitaj al la sama mesaĝo. La alŝuto malsukcesis. - Eraro dum sendo de la mesaĝo. + Okazis eraro dum la sendo de la mesaĝo. Hejmo Sciigoj Loka Fratara Rektaj mesaĝoj Langetoj - Mesaĝo + Fadeno Mesaĝoj Kun respondoj Alpinglitaj @@ -521,4 +521,35 @@ Kontroli la sciigojn Limigi sciigojn pri tempolinio La mesaĝo al kiu ĉi tiu malneto respondas estis forigita + %s registriĝis + iu registriĝis + 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. + Filmetoj kaj sondosieroj ne povas esti pli grandaj ol %s MB. + La bildo ne povis esti redaktita. + Ensaluti + Okazis eraro dum la sekvado de #%s + Okazos eraro dum la malsekvado de #%s + Ensalutu denove por ricevi sciigojn + %s redaktis sian mesaĝon + Fermi + Detaloj + Redaktitaj mesaĝoj + Sciigoj kiam mesaĝoj kun kiuj vi interagis estas redaktitaj + Redakti la bildon + 14 tagoj + 30 tagoj + 60 tagoj + 90 tagoj + 180 tagoj + 365 tagoj + Ekverki mesaĝon + Aliĝis je %1$s + Registras la malneton… + Ensalutu denove al ĉiuj kontoj por ŝalti sciigojn. + La salutpaĝo ne povis esti ŝargita. + Ŝargo de detaloj pri la konto malsukcesis \ No newline at end of file From a7c77e44859bed1e7970213645eaf3f72486639d Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 20 Aug 2022 14:26:38 +0000 Subject: [PATCH 056/142] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (491 of 491 strings) Co-authored-by: Eric Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/ Translation: Tusky/Tusky --- app/src/main/res/values-zh-rCN/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index f9154ac80..cd316f0b2 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -547,4 +547,6 @@ 音视频文件大小不能超出 %s MB。 关注 #%s 出错 取关 #%s 出错 + %s (%s) + (无更改) \ No newline at end of file From 27e5c15958dca75cce5895656c9a13f77ed2cdd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Sat, 20 Aug 2022 14:26:38 +0000 Subject: [PATCH 057/142] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (491 of 491 strings) Co-authored-by: Hồ Nhất Duy Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ Translation: Tusky/Tusky --- app/src/main/res/values-vi/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 925476319..5e1f19b44 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -528,4 +528,6 @@ Video và audio không thể quá %s MB. Lỗi khi theo dõi #%s Lỗi khi bỏ theo dõi #%s + %s (%s) + (Không đổi) \ No newline at end of file From 070b60de10f1f58ee6e9c66b8bf76aa80c0c42e6 Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Sat, 20 Aug 2022 14:26:38 +0000 Subject: [PATCH 058/142] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (491 of 491 strings) Co-authored-by: Vegard Skjefstad Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nb_NO/ Translation: Tusky/Tusky --- app/src/main/res/values-nb-rNO/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index a75717e9b..529b1dec1 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -539,4 +539,6 @@ 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 + %s (%s) + (Ingen endring) \ No newline at end of file From a47d8719553c93b26a359814fcdb368d1c071f34 Mon Sep 17 00:00:00 2001 From: Tobiasz Kubisiowski Date: Thu, 18 Aug 2022 13:27:13 +0000 Subject: [PATCH 059/142] Translated using Weblate (Esperanto) Currently translated at 16.6% (3 of 18 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/eo/ --- fastlane/metadata/android/eo/full_description.txt | 12 ++++++++++++ fastlane/metadata/android/eo/short_description.txt | 1 + fastlane/metadata/android/eo/title.txt | 1 + 3 files changed, 14 insertions(+) create mode 100644 fastlane/metadata/android/eo/full_description.txt create mode 100644 fastlane/metadata/android/eo/short_description.txt create mode 100644 fastlane/metadata/android/eo/title.txt diff --git a/fastlane/metadata/android/eo/full_description.txt b/fastlane/metadata/android/eo/full_description.txt new file mode 100644 index 000000000..c40ae16eb --- /dev/null +++ b/fastlane/metadata/android/eo/full_description.txt @@ -0,0 +1,12 @@ +Tusky estas malpeza klienta aplikaĵo por Mastodon, libera kaj malfermitkoda socireta servilo. + +• Material Design +• Funkcias kun la plejmulto de API de Mastodon +• Subtenas uzadon de pluraj uzantkontoj +• Hela kaj malhela etosoj kun la eblo de aŭtomata ŝanĝo inter ili depende de la momento de la tago +• Malnetoj – ekredaktu hupojn kaj konservu ilin por poste +• Elektu inter malsamaj emoĝi-stiloj +• Optimumigita por ĉiuj ekrangrandoj +• Tute malfermitkoda – sen ajna dependo de fermitkodaj servoj kiel tiuj de Google + +Por ekscii pli pri Mastodon, vizitu https://joinmastodon.org/ diff --git a/fastlane/metadata/android/eo/short_description.txt b/fastlane/metadata/android/eo/short_description.txt new file mode 100644 index 000000000..88d8f8ae3 --- /dev/null +++ b/fastlane/metadata/android/eo/short_description.txt @@ -0,0 +1 @@ +Pluruzanta aplikaĵo por la socia reto Mastodon diff --git a/fastlane/metadata/android/eo/title.txt b/fastlane/metadata/android/eo/title.txt new file mode 100644 index 000000000..0238ffc0a --- /dev/null +++ b/fastlane/metadata/android/eo/title.txt @@ -0,0 +1 @@ +Tusky From c638ad7e6bac539123c48a06c258838aa01e0af0 Mon Sep 17 00:00:00 2001 From: Ivan Kupalov Date: Mon, 22 Aug 2022 16:18:45 +0200 Subject: [PATCH 060/142] Make CaptionDialog non-cancellable, fix #2626 (#2676) This prevents accidental closing of dialog by clicking outside of the dialog. --- .../tusky/components/compose/dialog/CaptionDialog.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt index 71789611b..25b1d260d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt @@ -89,6 +89,7 @@ fun T.makeCaptionDialog( .setView(dialogLayout) .setPositiveButton(android.R.string.ok, okListener) .setNegativeButton(android.R.string.cancel, null) + .setCancelable(false) .create() val window = dialog.window From 0041acf2d429d9a0f275507ae35a07689427924d Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Wed, 31 Aug 2022 18:53:57 +0200 Subject: [PATCH 061/142] Add language dropdown to compose view (#2651) * Add UI for selecting post language * Apply selected language when sending status * Save/restore post language with drafts * Fall back to english if the configured language isn't found in the locale list (no-NB) * Remove comment about no_NB * Move language dropdown to top of compose view * Preserve language when redrafting * Set default language to target post's language when replying * Add Tusky license header to new source file * Tweak language dropdown button width --- .../42.json | 953 ++++++++++++++++++ .../tusky/adapter/LocaleAdapter.kt | 43 + .../components/compose/ComposeActivity.kt | 44 +- .../components/compose/ComposeViewModel.kt | 8 +- .../conversation/ConversationEntity.kt | 9 +- .../conversation/ConversationViewData.kt | 3 +- .../tusky/components/drafts/DraftHelper.kt | 6 +- .../tusky/components/drafts/DraftsActivity.kt | 6 +- .../notifications/NotificationHelper.java | 1 + .../fragments/SearchStatusesFragment.kt | 6 +- .../timeline/TimelineTypeMappers.kt | 11 +- .../keylesspalace/tusky/db/AppDatabase.java | 11 +- .../com/keylesspalace/tusky/db/DraftEntity.kt | 1 + .../tusky/db/TimelineStatusEntity.kt | 1 + .../com/keylesspalace/tusky/di/AppModule.kt | 1 + .../tusky/entity/DeletedStatus.kt | 3 +- .../keylesspalace/tusky/entity/NewStatus.kt | 3 +- .../com/keylesspalace/tusky/entity/Status.kt | 6 +- .../tusky/fragment/SFragment.java | 2 + .../receiver/SendStatusBroadcastReceiver.kt | 3 +- .../tusky/service/SendStatusService.kt | 9 +- app/src/main/res/layout/activity_compose.xml | 13 + app/src/main/res/values/strings.xml | 1 + .../tusky/BottomSheetActivityTest.kt | 3 +- .../tusky/ComposeActivityTest.kt | 14 + .../com/keylesspalace/tusky/FilterTest.kt | 3 +- .../tusky/components/timeline/StatusMocker.kt | 3 +- .../keylesspalace/tusky/db/TimelineDaoTest.kt | 1 + 28 files changed, 1140 insertions(+), 28 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/42.json create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/42.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/42.json new file mode 100644 index 000000000..a47f993a6 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/42.json @@ -0,0 +1,953 @@ +{ + "formatVersion": 1, + "database": { + "version": 42, + "identityHash": "a62399cb3859de7fcbb9bd7053f7cb1d", + "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, `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": "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, 'a62399cb3859de7fcbb9bd7053f7cb1d')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt new file mode 100644 index 000000000..fdde2f21b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt @@ -0,0 +1,43 @@ +/* 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.adapter + +import android.content.Context +import android.graphics.Typeface +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.TextView +import com.keylesspalace.tusky.util.ThemeUtils +import java.util.Locale + +class LocaleAdapter(context: Context, resource: Int, locales: List) : ArrayAdapter(context, resource, locales) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + 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() + } + } + + 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)})" + } + } +} 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 832eee813..c7e6f4a26 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 @@ -35,6 +35,7 @@ import android.view.KeyEvent import android.view.MenuItem import android.view.View import android.view.ViewGroup +import android.widget.AdapterView import android.widget.ImageButton import android.widget.LinearLayout import android.widget.PopupMenu @@ -65,6 +66,7 @@ import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.EmojiAdapter +import com.keylesspalace.tusky.adapter.LocaleAdapter import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog @@ -244,6 +246,7 @@ class ComposeActivity : binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt) } + setupLanguageSpinner(getInitialLanguage(composeOptions?.language)) setupComposeField(preferences, viewModel.startingText) setupContentWarningField(composeOptions?.contentWarning) setupPollView() @@ -476,6 +479,40 @@ class ComposeActivity : binding.addPollTextActionTextView.setOnClickListener { openPollDialog() } } + 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 + 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 + } + + override fun onNothingSelected(parent: AdapterView<*>) { + parent.setSelection(locales.indexOfFirst { it.language == getInitialLanguage() }) + } + } + 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 + } + } + private fun setupActionBar() { setSupportActionBar(binding.toolbar) supportActionBar?.run { @@ -793,6 +830,10 @@ class ComposeActivity : return length } + @VisibleForTesting + val selectedLanguage: String? + get() = viewModel.postLanguage + private fun updateVisibleCharactersLeft() { val remainingLength = maximumTootCharacters - calculateTextLength() binding.composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength) @@ -1128,7 +1169,8 @@ class ComposeActivity : var scheduledAt: String? = null, var sensitive: Boolean? = null, var poll: NewPoll? = null, - var modifiedInitialState: Boolean? = null + var modifiedInitialState: Boolean? = null, + var language: String? = null, ) : Parcelable companion object { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 564c15515..c650f72db 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -68,6 +68,7 @@ class ComposeViewModel @Inject constructor( private var replyingStatusAuthor: String? = null private var replyingStatusContent: String? = null internal var startingText: String? = null + internal var postLanguage: String? = null private var draftId: Int = 0 private var scheduledTootId: String? = null private var startingContentWarning: String = "" @@ -261,7 +262,8 @@ class ComposeViewModel @Inject constructor( mediaDescriptions = mediaDescriptions, poll = poll.value, failedToSend = false, - scheduledAt = scheduledAt.value + scheduledAt = scheduledAt.value, + language = postLanguage, ) } @@ -308,7 +310,8 @@ class ComposeViewModel @Inject constructor( draftId = draftId, idempotencyKey = randomAlphanumericString(16), retries = 0, - mediaProcessed = mediaProcessed + mediaProcessed = mediaProcessed, + language = postLanguage, ) serviceClient.sendToot(tootToSend) @@ -426,6 +429,7 @@ class ComposeViewModel @Inject constructor( draftId = composeOptions?.draftId ?: 0 scheduledTootId = composeOptions?.scheduledTootId startingText = composeOptions?.content + postLanguage = composeOptions?.language val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN if (tootVisibility.num != Status.Visibility.UNKNOWN.num) { 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 401d61463..6816c13be 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 @@ -94,7 +94,8 @@ data class ConversationStatusEntity( val expanded: Boolean, val collapsed: Boolean, val muted: Boolean, - val poll: Poll? + val poll: Poll?, + val language: String?, ) { fun toViewData(): StatusViewData.Concrete { @@ -125,7 +126,8 @@ data class ConversationStatusEntity( pinned = false, muted = muted, poll = poll, - card = null + card = null, + language = language, ), isExpanded = expanded, isShowingContent = showingHiddenContent, @@ -167,7 +169,8 @@ fun Status.toEntity() = expanded = false, collapsed = true, muted = muted ?: false, - poll = poll + poll = poll, + language = language, ) fun Conversation.toEntity(accountId: Long, order: Int) = 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 fae55f0ba..f9082e8a3 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 @@ -85,6 +85,7 @@ fun StatusViewData.Concrete.toConversationStatusEntity( expanded = expanded, collapsed = collapsed, muted = muted, - poll = poll + poll = poll, + language = status.language, ) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt index 1c41132ea..c3714371f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt @@ -61,7 +61,8 @@ class DraftHelper @Inject constructor( mediaDescriptions: List, poll: NewPoll?, failedToSend: Boolean, - scheduledAt: String? + scheduledAt: String?, + language: String?, ) = withContext(Dispatchers.IO) { val externalFilesDir = context.getExternalFilesDir("Tusky") @@ -118,7 +119,8 @@ class DraftHelper @Inject constructor( attachments = attachments, poll = poll, failedToSend = failedToSend, - scheduledAt = scheduledAt + scheduledAt = scheduledAt, + language = language, ) draftDao.insertOrReplace(draft) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt index deae29f4c..dfa361684 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt @@ -107,7 +107,8 @@ class DraftsActivity : BaseActivity(), DraftActionListener { poll = draft.poll, sensitive = draft.sensitive, visibility = draft.visibility, - scheduledAt = draft.scheduledAt + scheduledAt = draft.scheduledAt, + language = draft.language, ) bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN @@ -145,7 +146,8 @@ class DraftsActivity : BaseActivity(), DraftActionListener { poll = draft.poll, sensitive = draft.sensitive, visibility = draft.visibility, - scheduledAt = draft.scheduledAt + scheduledAt = draft.scheduledAt, + language = draft.language, ) startActivity(ComposeActivity.startIntent(this, composeOptions)) 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 620d49a9a..615411384 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 @@ -365,6 +365,7 @@ public class NotificationHelper { composeOptions.setReplyingStatusContent(citedText); composeOptions.setMentionedUsernames(mentionedUsernames); composeOptions.setModifiedInitialState(true); + composeOptions.setLanguage(actionableStatus.getLanguage()); Intent composeIntent = ComposeActivity.startIntent( context, 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 63bc0b648..a799bee3c 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 @@ -216,7 +216,8 @@ class SearchStatusesFragment : SearchFragment(), Status contentWarning = actionableStatus.spoilerText, mentionedUsernames = mentionedUsernames, replyingStatusAuthor = actionableStatus.account.localUsername, - replyingStatusContent = status.content.toString() + replyingStatusContent = status.content.toString(), + language = actionableStatus.language, ) ) bottomSheetActivity?.startActivityWithSlideInAnimation(intent) @@ -461,7 +462,8 @@ class SearchStatusesFragment : SearchFragment(), Status contentWarning = redraftStatus.spoilerText, mediaAttachments = redraftStatus.attachments, sensitive = redraftStatus.sensitive, - poll = redraftStatus.poll?.toNewPoll(status.createdAt) + poll = redraftStatus.poll?.toNewPoll(status.createdAt), + language = redraftStatus.language, ) ) startActivity(intent) 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 8b96283fd..25ca65bd7 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 @@ -99,7 +99,8 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity { contentShowing = false, pinned = false, card = null, - repliesCount = 0 + repliesCount = 0, + language = null, ) } @@ -141,7 +142,8 @@ fun Status.toEntity( contentCollapsed = contentCollapsed, pinned = actionableStatus.pinned == true, card = actionableStatus.card?.let(gson::toJson), - repliesCount = actionableStatus.repliesCount + repliesCount = actionableStatus.repliesCount, + language = actionableStatus.language, ) } @@ -185,7 +187,8 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { muted = status.muted, poll = poll, card = card, - repliesCount = status.repliesCount + repliesCount = status.repliesCount, + language = status.language, ) } val status = if (reblog != null) { @@ -216,6 +219,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { poll = null, card = null, repliesCount = status.repliesCount, + language = status.language, ) } else { Status( @@ -245,6 +249,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { poll = poll, card = card, repliesCount = status.repliesCount, + language = status.language, ) } return StatusViewData.Concrete( 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 c4b672e4d..ee365c4b3 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 = 41) + }, version = 42) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -601,4 +601,13 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("ALTER TABLE `DraftEntity` ADD COLUMN `scheduledAt` TEXT"); } }; + + public static final Migration MIGRATION_41_42 = new Migration(41, 42) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `DraftEntity` ADD COLUMN `language` TEXT"); + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `language` TEXT"); + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_language` TEXT"); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt index 41565b07c..5479fd886 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftEntity.kt @@ -40,6 +40,7 @@ data class DraftEntity( val poll: NewPoll?, val failedToSend: Boolean, val scheduledAt: String?, + val language: String?, ) /** 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 ecd3c0ce5..4c4340998 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt @@ -81,6 +81,7 @@ data class TimelineStatusEntity( val contentShowing: Boolean, val pinned: Boolean, val card: String?, + val language: String?, ) @Entity( 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 b2f7d7b7b..6b70cc27f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -66,6 +66,7 @@ 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, ) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt index 92a35b69c..a417f3165 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt @@ -27,7 +27,8 @@ data class DeletedStatus( val sensitive: Boolean, @SerializedName("media_attachments") var attachments: ArrayList?, val poll: Poll?, - @SerializedName("created_at") val createdAt: Date + @SerializedName("created_at") val createdAt: Date, + val language: String?, ) { fun isEmpty(): Boolean { return text == null && attachments == null diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt index 83ed56e95..d11ad5f7a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt @@ -27,7 +27,8 @@ data class NewStatus( val sensitive: Boolean, @SerializedName("media_ids") val mediaIds: List?, @SerializedName("scheduled_at") val scheduledAt: String?, - val poll: NewPoll? + val poll: NewPoll?, + val language: String?, ) @Parcelize 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 72a37f913..93386713c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -48,7 +48,8 @@ data class Status( val pinned: Boolean?, val muted: Boolean?, val poll: Poll?, - val card: Card? + val card: Card?, + val language: String?, ) { val actionableId: String @@ -130,7 +131,8 @@ data class Status( sensitive = sensitive, attachments = attachments, poll = poll, - createdAt = createdAt + createdAt = createdAt, + language = language, ) } 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 fe94dbf8d..c859cb2a8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -154,6 +154,7 @@ public abstract class SFragment extends Fragment implements Injectable { composeOptions.setMentionedUsernames(mentionedUsernames); composeOptions.setReplyingStatusAuthor(actionableStatus.getAccount().getLocalUsername()); composeOptions.setReplyingStatusContent(parseAsMastodonHtml(actionableStatus.getContent()).toString()); + composeOptions.setLanguage(actionableStatus.getLanguage()); Intent intent = ComposeActivity.startIntent(getContext(), composeOptions); getActivity().startActivity(intent); @@ -425,6 +426,7 @@ public abstract class SFragment extends Fragment implements Injectable { composeOptions.setMediaAttachments(deletedStatus.getAttachments()); composeOptions.setSensitive(deletedStatus.getSensitive()); composeOptions.setModifiedInitialState(true); + composeOptions.setLanguage(deletedStatus.getLanguage()); if (deletedStatus.getPoll() != null) { composeOptions.setPoll(deletedStatus.getPoll().toNewPoll(deletedStatus.getCreatedAt())); } diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt index 75ccf799a..40b2771a1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -98,7 +98,8 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { draftId = -1, idempotencyKey = randomAlphanumericString(16), retries = 0, - mediaProcessed = mutableListOf() + mediaProcessed = mutableListOf(), + null, ) ) diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt index 9471ee2fa..899fab485 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt @@ -156,7 +156,8 @@ class SendStatusService : Service(), Injectable { statusToSend.sensitive, statusToSend.mediaIds, statusToSend.scheduledAt, - statusToSend.poll + statusToSend.poll, + statusToSend.language, ) mastodonApi.createStatus( @@ -259,7 +260,8 @@ class SendStatusService : Service(), Injectable { mediaDescriptions = status.mediaDescriptions, poll = status.poll, failedToSend = true, - scheduledAt = status.scheduledAt + scheduledAt = status.scheduledAt, + language = status.language, ) } @@ -366,5 +368,6 @@ data class StatusToSend( val draftId: Int, val idempotencyKey: String, var retries: Int, - val mediaProcessed: MutableList + val mediaProcessed: MutableList, + val language: String?, ) : Parcelable diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml index 958de1b8d..b2d734ea1 100644 --- a/app/src/main/res/layout/activity_compose.xml +++ b/app/src/main/res/layout/activity_compose.xml @@ -22,6 +22,19 @@ tools:ignore="ContentDescription" /> + + Poll with choices: %1$s, %2$s, %3$s, %4$s; %5$s + Post language List name diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt index 503a03176..4c7779997 100644 --- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt @@ -88,7 +88,8 @@ class BottomSheetActivityTest { pinned = false, muted = false, poll = null, - card = null + card = null, + language = null, ) private val statusSingle = Single.just(SearchResult(emptyList(), listOf(status), emptyList())) diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index 2c20cb78e..6542b603d 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -48,6 +48,7 @@ import org.robolectric.Robolectric import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config import org.robolectric.fakes.RoboMenuItem +import java.util.Locale /** * Created by charlag on 3/7/18. @@ -444,6 +445,19 @@ class ComposeActivityTest { assertEquals(selectionEnd + insertText.length, editor.selectionEnd) } + @Test + fun whenNoLanguageIsGiven_defaultLanguageIsSelected() { + assertEquals(Locale.getDefault().language, activity.selectedLanguage) + } + + @Test + fun languageGivenInComposeOptionsIsRespected() { + val language = "no" + 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 48139fc4e..b28f6c9a0 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt @@ -260,7 +260,8 @@ class FilterTest { ownVotes = null ) } else null, - card = null + card = null, + language = null, ) } } 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 fc8e15b34..ad1d4ae69 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 @@ -51,7 +51,8 @@ fun mockStatus( pinned = false, muted = false, poll = null, - card = null + card = null, + language = null, ) fun mockStatusViewData( 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 b629f9d86..a05f39a6f 100644 --- a/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt @@ -463,6 +463,7 @@ class TimelineDaoTest { contentShowing = true, pinned = false, card = card, + language = null, ) return Triple(status, author, reblogAuthor) } From 4665637086a4ca992c6e3f4b392c97451b28b5c7 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Wed, 31 Aug 2022 18:54:40 +0200 Subject: [PATCH 062/142] make all model classes immutable (#2686) --- .../components/search/SearchViewModel.kt | 73 ++++++++----------- .../tusky/entity/Announcement.kt | 4 +- .../tusky/entity/DeletedStatus.kt | 6 +- .../com/keylesspalace/tusky/entity/Status.kt | 12 +-- 4 files changed, 41 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index 738c662ab..84a8b0325 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -24,7 +24,6 @@ import com.keylesspalace.tusky.components.search.adapter.SearchPagingSourceFacto import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.entity.DeletedStatus -import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.usecase.TimelineCases @@ -113,11 +112,7 @@ class SearchViewModel @Inject constructor( } fun expandedChange(statusViewData: StatusViewData.Concrete, expanded: Boolean) { - val idx = loadedStatuses.indexOf(statusViewData) - if (idx >= 0) { - loadedStatuses[idx] = statusViewData.copy(isExpanded = expanded) - statusesPagingSourceFactory.invalidate() - } + updateStatusViewData(statusViewData.copy(isExpanded = expanded)) } fun reblog(statusViewData: StatusViewData.Concrete, reblog: Boolean) { @@ -131,51 +126,34 @@ class SearchViewModel @Inject constructor( } private fun setRebloggedForStatus(statusViewData: StatusViewData.Concrete, reblog: Boolean) { - statusViewData.status.reblogged = reblog - statusViewData.status.reblog?.reblogged = reblog - statusesPagingSourceFactory.invalidate() + updateStatus( + statusViewData.status.copy( + reblogged = reblog, + reblog = statusViewData.status.reblog?.copy(reblogged = reblog) + ) + ) } fun contentHiddenChange(statusViewData: StatusViewData.Concrete, isShowing: Boolean) { - val idx = loadedStatuses.indexOf(statusViewData) - if (idx >= 0) { - loadedStatuses[idx] = statusViewData.copy(isShowingContent = isShowing) - statusesPagingSourceFactory.invalidate() - } + updateStatusViewData(statusViewData.copy(isShowingContent = isShowing)) } fun collapsedChange(statusViewData: StatusViewData.Concrete, collapsed: Boolean) { - val idx = loadedStatuses.indexOf(statusViewData) - if (idx >= 0) { - loadedStatuses[idx] = statusViewData.copy(isCollapsed = collapsed) - statusesPagingSourceFactory.invalidate() - } + updateStatusViewData(statusViewData.copy(isCollapsed = collapsed)) } fun voteInPoll(statusViewData: StatusViewData.Concrete, choices: MutableList) { val votedPoll = statusViewData.status.actionableStatus.poll!!.votedCopy(choices) - updateStatus(statusViewData, votedPoll) + updateStatus(statusViewData.status.copy(poll = votedPoll)) timelineCases.voteInPoll(statusViewData.id, votedPoll.id, choices) .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { newPoll -> updateStatus(statusViewData, newPoll) }, - { t -> Log.d(TAG, "Failed to vote in poll: ${statusViewData.id}", t) } - ) + .doOnError { t -> Log.d(TAG, "Failed to vote in poll: ${statusViewData.id}", t) } + .subscribe() .autoDispose() } - private fun updateStatus(statusViewData: StatusViewData.Concrete, newPoll: Poll) { - val idx = loadedStatuses.indexOf(statusViewData) - if (idx >= 0) { - val newStatus = statusViewData.status.copy(poll = newPoll) - loadedStatuses[idx] = statusViewData.copy(status = newStatus) - statusesPagingSourceFactory.invalidate() - } - } - fun favorite(statusViewData: StatusViewData.Concrete, isFavorited: Boolean) { - statusViewData.status.favourited = isFavorited - statusesPagingSourceFactory.invalidate() + updateStatus(statusViewData.status.copy(favourited = isFavorited)) timelineCases.favourite(statusViewData.id, isFavorited) .onErrorReturnItem(statusViewData.status) .subscribe() @@ -183,8 +161,7 @@ class SearchViewModel @Inject constructor( } fun bookmark(statusViewData: StatusViewData.Concrete, isBookmarked: Boolean) { - statusViewData.status.bookmarked = isBookmarked - statusesPagingSourceFactory.invalidate() + updateStatus(statusViewData.status.copy(bookmarked = isBookmarked)) timelineCases.bookmark(statusViewData.id, isBookmarked) .onErrorReturnItem(statusViewData.status) .subscribe() @@ -208,18 +185,28 @@ class SearchViewModel @Inject constructor( } fun muteConversation(statusViewData: StatusViewData.Concrete, mute: Boolean) { - val idx = loadedStatuses.indexOf(statusViewData) - if (idx >= 0) { - val newStatus = statusViewData.status.copy(muted = mute) - loadedStatuses[idx] = statusViewData.copy(status = newStatus) - statusesPagingSourceFactory.invalidate() - } + updateStatus(statusViewData.status.copy(muted = mute)) timelineCases.muteConversation(statusViewData.id, mute) .onErrorReturnItem(statusViewData.status) .subscribe() .autoDispose() } + private fun updateStatusViewData(newStatusViewData: StatusViewData.Concrete) { + val idx = loadedStatuses.indexOfFirst { it.id == newStatusViewData.id } + if (idx >= 0) { + loadedStatuses[idx] = newStatusViewData + statusesPagingSourceFactory.invalidate() + } + } + + private fun updateStatus(newStatus: Status) { + val statusViewData = loadedStatuses.find { it.id == newStatus.id } + if (statusViewData != null) { + updateStatusViewData(statusViewData.copy(status = newStatus)) + } + } + companion object { private const val TAG = "SearchViewModel" private const val DEFAULT_LOAD_SIZE = 20 diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt index 00d5659d5..5837815b0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt @@ -48,8 +48,8 @@ data class Announcement( data class Reaction( val name: String, - var count: Int, - var me: Boolean, + val count: Int, + val me: Boolean, val url: String?, @SerializedName("static_url") val staticUrl: String? ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt index a417f3165..a653cc587 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt @@ -20,12 +20,12 @@ import java.util.ArrayList import java.util.Date data class DeletedStatus( - var text: String?, - @SerializedName("in_reply_to_id") var inReplyToId: String?, + val text: String?, + @SerializedName("in_reply_to_id") val inReplyToId: String?, @SerializedName("spoiler_text") val spoilerText: String, val visibility: Status.Visibility, val sensitive: Boolean, - @SerializedName("media_attachments") var attachments: ArrayList?, + @SerializedName("media_attachments") val attachments: ArrayList?, val poll: Poll?, @SerializedName("created_at") val createdAt: Date, val language: String?, 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 93386713c..c147ae30c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -26,7 +26,7 @@ data class Status( val id: String, val url: String?, // not present if it's reblog val account: TimelineAccount, - @SerializedName("in_reply_to_id") var inReplyToId: String?, + @SerializedName("in_reply_to_id") val inReplyToId: String?, @SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?, val reblog: Status?, val content: String, @@ -35,13 +35,13 @@ data class Status( @SerializedName("reblogs_count") val reblogsCount: Int, @SerializedName("favourites_count") val favouritesCount: Int, @SerializedName("replies_count") val repliesCount: Int, - var reblogged: Boolean, - var favourited: Boolean, - var bookmarked: Boolean, - var sensitive: Boolean, + val reblogged: Boolean, + val favourited: Boolean, + val bookmarked: Boolean, + val sensitive: Boolean, @SerializedName("spoiler_text") val spoilerText: String, val visibility: Visibility, - @SerializedName("media_attachments") var attachments: ArrayList, + @SerializedName("media_attachments") val attachments: ArrayList, val mentions: List, val tags: List?, val application: Application?, From 257f3a5c1c2f069ea9b92924f3caaf82618360d8 Mon Sep 17 00:00:00 2001 From: Ivan Kupalov Date: Wed, 31 Aug 2022 18:55:27 +0200 Subject: [PATCH 063/142] Fix broken timeline when there are only expired filters (#2689) * Fix broken timeline when there are only expired filters The issue happened when the only applicable filters are expired. There was a check to not produce an empty regex when there are no filters but it was done before removing expired filters so we would produce an empty regex that would match (and remove) everything and the timeline would get stuck. * Make a mini-optimization for FilterModel --- .../com/keylesspalace/tusky/network/FilterModel.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt b/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt index 8adad95f6..db47beaf0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt @@ -38,7 +38,8 @@ class FilterModel @Inject constructor() { (spoilerText.isNotEmpty() && matcher.reset(spoilerText).find()) || ( attachmentsDescriptions.isNotEmpty() && - matcher.reset(attachmentsDescriptions.joinToString("\n")).find() + matcher.reset(attachmentsDescriptions.joinToString("\n")) + .find() ) ) } @@ -54,9 +55,10 @@ class FilterModel @Inject constructor() { } private fun makeFilter(filters: List): Pattern? { - if (filters.isEmpty()) return null - val tokens = filters - .filter { it.expiresAt?.before(Date()) != true } + val now = Date() + val nonExpiredFilters = filters.filter { it.expiresAt?.before(now) != true } + if (nonExpiredFilters.isEmpty()) return null + val tokens = nonExpiredFilters .map { filterToRegexToken(it) } return Pattern.compile(TextUtils.join("|", tokens), Pattern.CASE_INSENSITIVE) From c8fc2418b8f5458a817bba221d025b822225e130 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 2 Sep 2022 16:52:47 +0200 Subject: [PATCH 064/142] AccountMediaFragment improvements (#2684) * initial setup * add spacing between images * use blurhash * handle hidden state and show video indicator * handle item clicks * small cleanup * move SquareImageView into account.media package * fix build * improve AccountMediaGridAdapter * handle loadstate, errors and refreshing * remove commented out code * log error * show audio attachments with icon * fix glitchy transition animation * set image Description on imageview * show toast with media description on long press --- .../tusky/adapter/StatusBaseViewHolder.java | 27 +- .../components/account/AccountPagerAdapter.kt | 2 +- .../account/media/AccountMediaFragment.kt | 328 +++++------------- .../account/media/AccountMediaGridAdapter.kt | 126 +++++++ .../account/media/AccountMediaPagingSource.kt | 37 ++ .../media/AccountMediaRemoteMediator.kt | 80 +++++ .../account/media/AccountMediaViewModel.kt | 64 ++++ .../media/GridSpacingItemDecoration.kt | 47 +++ .../account/media}/SquareImageView.kt | 2 +- .../tusky/di/ViewModelFactory.kt | 6 + .../tusky/network/MastodonApi.kt | 12 +- .../tusky/util/AttachmentHelper.kt | 26 ++ .../tusky/viewdata/AttachmentViewData.kt | 34 +- .../main/res/layout/item_account_media.xml | 18 + app/src/main/res/values/dimens.xml | 5 + 15 files changed, 530 insertions(+), 284 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaPagingSource.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaRemoteMediator.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaViewModel.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/account/media/GridSpacingItemDecoration.kt rename app/src/main/java/com/keylesspalace/tusky/{view => components/account/media}/SquareImageView.kt (92%) create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/AttachmentHelper.kt create mode 100644 app/src/main/res/layout/item_account_media.xml 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 980f644b0..51b61facc 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -29,7 +29,6 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.RequestBuilder; -import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.bumptech.glide.load.resource.bitmap.CenterCrop; import com.bumptech.glide.load.resource.bitmap.GranularRoundedCorners; import com.google.android.material.button.MaterialButton; @@ -44,6 +43,7 @@ import com.keylesspalace.tusky.entity.HashTag; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.util.AbsoluteTimeFormatter; +import com.keylesspalace.tusky.util.AttachmentHelper; import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper; @@ -563,7 +563,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { if (i < attachments.size()) { Attachment attachment = attachments.get(i); mediaLabel.setVisibility(View.VISIBLE); - mediaDescriptions[i] = getAttachmentDescription(context, attachment); + mediaDescriptions[i] = AttachmentHelper.getFormattedDescription(attachment, context); updateMediaLabel(i, sensitive, showingContent); // Set the icon next to the label. @@ -590,24 +590,12 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } }); view.setOnLongClickListener(v -> { - CharSequence description = getAttachmentDescription(view.getContext(), attachment); + CharSequence description = AttachmentHelper.getFormattedDescription(attachment, view.getContext()); Toast.makeText(view.getContext(), description, Toast.LENGTH_LONG).show(); return true; }); } - private static CharSequence getAttachmentDescription(Context context, Attachment attachment) { - String duration = ""; - if (attachment.getMeta() != null && attachment.getMeta().getDuration() != null && attachment.getMeta().getDuration() > 0) { - duration = formatDuration(attachment.getMeta().getDuration()) + " "; - } - if (TextUtils.isEmpty(attachment.getDescription())) { - return duration + context.getString(R.string.description_post_media_no_description_placeholder); - } else { - return duration + attachment.getDescription(); - } - } - protected void hideSensitiveMediaWarning() { sensitiveMediaWarning.setVisibility(View.GONE); sensitiveMediaShow.setVisibility(View.GONE); @@ -1168,13 +1156,4 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { bookmarkButton.setVisibility(visibility); moreButton.setVisibility(visibility); } - - private static String formatDuration(double durationInSeconds) { - int seconds = (int) Math.round(durationInSeconds) % 60; - int minutes = (int) durationInSeconds % 3600 / 60; - int hours = (int) durationInSeconds / 3600; - - return String.format("%d:%02d:%02d", hours, minutes, seconds); - } - } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountPagerAdapter.kt index 760db8294..baeeea43f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountPagerAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountPagerAdapter.kt @@ -35,7 +35,7 @@ class AccountPagerAdapter( 0 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER, accountId, false) 1 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_WITH_REPLIES, accountId, false) 2 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_PINNED, accountId, false) - 3 -> AccountMediaFragment.newInstance(accountId, false) + 3 -> AccountMediaFragment.newInstance(accountId) else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds") } } 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 57876d852..69d651d51 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 @@ -1,4 +1,4 @@ -/* Copyright 2017 Andrew Dawson +/* Copyright 2022 Tusky Contributors * * This file is a part of Tusky. * @@ -15,41 +15,35 @@ package com.keylesspalace.tusky.components.account.media -import android.graphics.Color import android.os.Bundle import android.util.Log import android.view.View -import android.view.ViewGroup -import android.widget.ImageView import androidx.core.app.ActivityOptionsCompat import androidx.core.view.ViewCompat import androidx.fragment.app.Fragment -import androidx.lifecycle.Lifecycle +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState +import androidx.preference.PreferenceManager import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import autodispose2.androidx.lifecycle.autoDispose -import com.bumptech.glide.Glide import com.keylesspalace.tusky.R import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.databinding.FragmentTimelineBinding +import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Attachment -import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.interfaces.RefreshableFragment -import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.ThemeUtils +import com.keylesspalace.tusky.settings.PrefKeys 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.view.SquareImageView +import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.viewdata.AttachmentViewData -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.core.SingleObserver -import io.reactivex.rxjava3.disposables.Disposable -import retrofit2.Response +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import java.io.IOException -import java.util.Random import javax.inject.Inject /** @@ -58,192 +52,98 @@ import javax.inject.Inject * Fragment with multiple columns of media previews for the specified account. */ -class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFragment, Injectable { +class AccountMediaFragment : + Fragment(R.layout.fragment_timeline), + RefreshableFragment, + Injectable { @Inject - lateinit var api: MastodonApi + lateinit var viewModelFactory: ViewModelFactory + + @Inject + lateinit var accountManager: AccountManager private val binding by viewBinding(FragmentTimelineBinding::bind) - private lateinit var accountId: String + private val viewModel: AccountMediaViewModel by viewModels { viewModelFactory } - private val adapter = MediaGridAdapter() - private val statuses = mutableListOf() - private var fetchingStatus = FetchingStatus.NOT_FETCHING - - private var isSwipeToRefreshEnabled: Boolean = true - private var needToRefresh = false - - private val callback = object : SingleObserver>> { - override fun onError(t: Throwable) { - fetchingStatus = FetchingStatus.NOT_FETCHING - - if (isAdded) { - binding.swipeRefreshLayout.isRefreshing = false - binding.progressBar.visibility = View.GONE - binding.topProgressBar.hide() - binding.statusView.show() - if (t is IOException) { - binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { - doInitialLoadingIfNeeded() - } - } else { - binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { - doInitialLoadingIfNeeded() - } - } - } - - Log.d(TAG, "Failed to fetch account media", t) - } - - override fun onSuccess(response: Response>) { - fetchingStatus = FetchingStatus.NOT_FETCHING - if (isAdded) { - binding.swipeRefreshLayout.isRefreshing = false - binding.progressBar.visibility = View.GONE - binding.topProgressBar.hide() - - val body = response.body() - body?.let { fetched -> - statuses.addAll(0, fetched) - // flatMap requires iterable but I don't want to box each array into list - val result = mutableListOf() - for (status in fetched) { - result.addAll(AttachmentViewData.list(status)) - } - adapter.addTop(result) - if (result.isNotEmpty()) - binding.recyclerView.scrollToPosition(0) - - if (statuses.isEmpty()) { - binding.statusView.show() - binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty) - } - } - } - } - - override fun onSubscribe(d: Disposable) {} - } - - private val bottomCallback = object : SingleObserver>> { - override fun onError(t: Throwable) { - fetchingStatus = FetchingStatus.NOT_FETCHING - - Log.d(TAG, "Failed to fetch account media", t) - } - - override fun onSuccess(response: Response>) { - fetchingStatus = FetchingStatus.NOT_FETCHING - val body = response.body() - body?.let { fetched -> - Log.d(TAG, "fetched ${fetched.size} statuses") - if (fetched.isNotEmpty()) Log.d(TAG, "first: ${fetched.first().id}, last: ${fetched.last().id}") - statuses.addAll(fetched) - Log.d(TAG, "now there are ${statuses.size} statuses") - // flatMap requires iterable but I don't want to box each array into list - val result = mutableListOf() - for (status in fetched) { - result.addAll(AttachmentViewData.list(status)) - } - adapter.addBottom(result) - } - } - - override fun onSubscribe(d: Disposable) { } - } + private lateinit var adapter: AccountMediaGridAdapter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) == true - accountId = arguments?.getString(ACCOUNT_ID_ARG)!! + viewModel.accountId = arguments?.getString(ACCOUNT_ID_ARG)!! } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + + val alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia + + val preferences = PreferenceManager.getDefaultSharedPreferences(view.context) + val useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true) + + adapter = AccountMediaGridAdapter( + alwaysShowSensitiveMedia = alwaysShowSensitiveMedia, + useBlurhash = useBlurhash, + context = view.context, + onAttachmentClickListener = ::onAttachmentClick + ) val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count) - val layoutManager = GridLayoutManager(view.context, columnCount) + val imageSpacing = view.context.resources.getDimensionPixelSize(R.dimen.profile_media_spacing) - adapter.baseItemColor = ThemeUtils.getColor(view.context, android.R.attr.windowBackground) + binding.recyclerView.addItemDecoration(GridSpacingItemDecoration(columnCount, imageSpacing, 0)) - binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.layoutManager = GridLayoutManager(view.context, columnCount) binding.recyclerView.adapter = adapter - if (isSwipeToRefreshEnabled) { - binding.swipeRefreshLayout.setOnRefreshListener { - refresh() - } - binding.swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue) - } + binding.swipeRefreshLayout.isEnabled = false + binding.statusView.visibility = View.GONE - binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - - override fun onScrolled(recycler_view: RecyclerView, dx: Int, dy: Int) { - if (dy > 0) { - val itemCount = layoutManager.itemCount - val lastItem = layoutManager.findLastCompletelyVisibleItemPosition() - if (itemCount <= lastItem + 3 && fetchingStatus == FetchingStatus.NOT_FETCHING) { - statuses.lastOrNull()?.let { (id) -> - Log.d(TAG, "Requesting statuses with max_id: $id, (bottom)") - fetchingStatus = FetchingStatus.FETCHING_BOTTOM - api.accountStatuses(accountId, id, null, null, null, true, null) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY) - .subscribe(bottomCallback) - } - } - } + viewLifecycleOwner.lifecycleScope.launch { + viewModel.media.collectLatest { pagingData -> + adapter.submitData(pagingData) + } + } + + adapter.addLoadStateListener { loadState -> + binding.progressBar.visible(loadState.refresh == LoadState.Loading && adapter.itemCount == 0) + + 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() } + } + Log.w(TAG, "error loading account media", errorState.error) + } else { + binding.recyclerView.show() + binding.statusView.hide() } - }) - - doInitialLoadingIfNeeded() - } - - private fun refresh() { - binding.statusView.hide() - if (fetchingStatus != FetchingStatus.NOT_FETCHING) return - if (statuses.isEmpty()) { - fetchingStatus = FetchingStatus.INITIAL_FETCHING - api.accountStatuses(accountId, null, null, null, null, true, null) - } else { - fetchingStatus = FetchingStatus.REFRESHING - api.accountStatuses(accountId, null, statuses[0].id, null, null, true, null) - }.observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe(callback) - - if (!isSwipeToRefreshEnabled) - binding.topProgressBar.show() - } - - private fun doInitialLoadingIfNeeded() { - if (isAdded) { - binding.statusView.hide() } - if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) { - fetchingStatus = FetchingStatus.INITIAL_FETCHING - api.accountStatuses(accountId, null, null, null, null, true, null) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this@AccountMediaFragment, Lifecycle.Event.ON_DESTROY) - .subscribe(callback) - } else if (needToRefresh) - refresh() - needToRefresh = false } - private fun viewMedia(items: List, currentIndex: Int, view: View?) { + private fun onAttachmentClick(selected: AttachmentViewData, view: View) { + if (!selected.isRevealed) { + viewModel.revealAttachment(selected) + return + } + val attachmentsFromSameStatus = viewModel.attachmentData.filter { attachmentViewData -> + attachmentViewData.statusId == selected.statusId + } + val currentIndex = attachmentsFromSameStatus.indexOf(selected) - when (items[currentIndex].attachment.type) { + when (selected.attachment.type) { Attachment.Type.IMAGE, Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.AUDIO -> { - val intent = ViewMediaActivity.newIntent(context, items, currentIndex) - if (view != null && activity != null) { - val url = items[currentIndex].attachment.url + val intent = ViewMediaActivity.newIntent(context, attachmentsFromSameStatus, currentIndex) + if (activity != null) { + val url = selected.attachment.url ViewCompat.setTransitionName(view, url) val options = ActivityOptionsCompat.makeSceneTransitionAnimation(requireActivity(), view, url) startActivity(intent, options.toBundle()) @@ -252,96 +152,26 @@ class AccountMediaFragment : Fragment(R.layout.fragment_timeline), RefreshableFr } } Attachment.Type.UNKNOWN -> { - context?.openLink(items[currentIndex].attachment.url) - } - } - } - - private enum class FetchingStatus { - NOT_FETCHING, INITIAL_FETCHING, FETCHING_BOTTOM, REFRESHING - } - - inner class MediaGridAdapter : - RecyclerView.Adapter() { - - var baseItemColor = Color.BLACK - - private val items = mutableListOf() - private val itemBgBaseHSV = FloatArray(3) - private val random = Random() - - fun addTop(newItems: List) { - items.addAll(0, newItems) - notifyItemRangeInserted(0, newItems.size) - } - - fun addBottom(newItems: List) { - if (newItems.isEmpty()) return - - val oldLen = items.size - items.addAll(newItems) - notifyItemRangeInserted(oldLen, newItems.size) - } - - override fun onAttachedToRecyclerView(recycler_view: RecyclerView) { - val hsv = FloatArray(3) - Color.colorToHSV(baseItemColor, hsv) - super.onAttachedToRecyclerView(recycler_view) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder { - val view = SquareImageView(parent.context) - view.scaleType = ImageView.ScaleType.CENTER_CROP - return MediaViewHolder(view) - } - - override fun getItemCount(): Int = items.size - - override fun onBindViewHolder(holder: MediaViewHolder, position: Int) { - itemBgBaseHSV[2] = random.nextFloat() * (1f - 0.3f) + 0.3f - holder.imageView.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV)) - val item = items[position] - - Glide.with(holder.imageView) - .load(item.attachment.previewUrl) - .centerInside() - .into(holder.imageView) - } - - inner class MediaViewHolder(val imageView: ImageView) : - RecyclerView.ViewHolder(imageView), - View.OnClickListener { - init { - itemView.setOnClickListener(this) - } - - // saving some allocations - override fun onClick(v: View?) { - viewMedia(items, bindingAdapterPosition, imageView) + context?.openLink(selected.attachment.url) } } } override fun refreshContent() { - if (isAdded) - refresh() - else - needToRefresh = true + adapter.refresh() } companion object { - @JvmStatic - fun newInstance(accountId: String, enableSwipeToRefresh: Boolean = true): AccountMediaFragment { + + fun newInstance(accountId: String): AccountMediaFragment { val fragment = AccountMediaFragment() - val args = Bundle() + val args = Bundle(1) args.putString(ACCOUNT_ID_ARG, accountId) - args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh) fragment.arguments = args return fragment } private const val ACCOUNT_ID_ARG = "account_id" private const val TAG = "AccountMediaFragment" - private const val ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh" } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt new file mode 100644 index 000000000..e5a0b592d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt @@ -0,0 +1,126 @@ +package com.keylesspalace.tusky.components.account.media + +import android.content.Context +import android.graphics.Color +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.view.setPadding +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemAccountMediaBinding +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.ThemeUtils +import com.keylesspalace.tusky.util.decodeBlurHash +import com.keylesspalace.tusky.util.getFormattedDescription +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import java.util.Random + +class AccountMediaGridAdapter( + private val alwaysShowSensitiveMedia: Boolean, + private val useBlurhash: Boolean, + context: Context, + private val onAttachmentClickListener: (AttachmentViewData, View) -> Unit +) : PagingDataAdapter>( + object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: AttachmentViewData, newItem: AttachmentViewData): Boolean { + return oldItem.attachment.id == newItem.attachment.id + } + + override fun areContentsTheSame(oldItem: AttachmentViewData, newItem: AttachmentViewData): Boolean { + return oldItem == newItem + } + } +) { + + private val baseItemBackgroundColor = ThemeUtils.getColor(context, R.attr.colorSurface) + private val videoIndicator = AppCompatResources.getDrawable(context, R.drawable.ic_play_indicator) + private val mediaHiddenDrawable = AppCompatResources.getDrawable(context, R.drawable.ic_hide_media_24dp) + + private val itemBgBaseHSV = FloatArray(3) + private val random = Random() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder { + val binding = ItemAccountMediaBinding.inflate(LayoutInflater.from(parent.context), parent, false) + Color.colorToHSV(baseItemBackgroundColor, itemBgBaseHSV) + itemBgBaseHSV[2] = itemBgBaseHSV[2] + random.nextFloat() / 3f - 1f / 6f + binding.root.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV)) + return BindingHolder(binding) + } + + override fun onBindViewHolder(holder: BindingHolder, position: Int) { + val context = holder.binding.root.context + getItem(position)?.let { item -> + + val imageView = holder.binding.accountMediaImageView + val overlay = holder.binding.accountMediaImageViewOverlay + + val blurhash = item.attachment.blurhash + val placeholder = if (useBlurhash && blurhash != null) { + decodeBlurHash(context, blurhash) + } else { + null + } + + if (item.attachment.type == Attachment.Type.AUDIO) { + overlay.hide() + + imageView.setPadding(context.resources.getDimensionPixelSize(R.dimen.profile_media_audio_icon_padding)) + + Glide.with(imageView) + .load(R.drawable.ic_music_box_preview_24dp) + .centerInside() + .into(imageView) + + imageView.contentDescription = item.attachment.getFormattedDescription(context) + } else if (item.sensitive && !item.isRevealed && !alwaysShowSensitiveMedia) { + overlay.show() + overlay.setImageDrawable(mediaHiddenDrawable) + + imageView.setPadding(0) + + Glide.with(imageView) + .load(placeholder) + .centerInside() + .into(imageView) + + imageView.contentDescription = imageView.context.getString(R.string.post_media_hidden_title) + } else { + if (item.attachment.type == Attachment.Type.VIDEO || item.attachment.type == Attachment.Type.GIFV) { + overlay.show() + overlay.setImageDrawable(videoIndicator) + } else { + overlay.hide() + } + + imageView.setPadding(0) + + Glide.with(imageView) + .asBitmap() + .load(item.attachment.previewUrl) + .placeholder(placeholder) + .centerInside() + .into(imageView) + + imageView.contentDescription = item.attachment.getFormattedDescription(context) + } + + holder.binding.root.setOnClickListener { + onAttachmentClickListener(item, imageView) + } + + holder.binding.root.setOnLongClickListener { view -> + val description = item.attachment.getFormattedDescription(view.context) + Toast.makeText(view.context, description, Toast.LENGTH_LONG).show() + true + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaPagingSource.kt new file mode 100644 index 000000000..60c767436 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaPagingSource.kt @@ -0,0 +1,37 @@ +/* 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.components.account.media + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.keylesspalace.tusky.viewdata.AttachmentViewData + +class AccountMediaPagingSource( + private val viewModel: AccountMediaViewModel +) : PagingSource() { + + override fun getRefreshKey(state: PagingState): String? = null + + override suspend fun load(params: LoadParams): LoadResult { + + return if (params is LoadParams.Refresh) { + val list = viewModel.attachmentData.toList() + LoadResult.Page(list, null, list.lastOrNull()?.statusId) + } else { + LoadResult.Page(emptyList(), null, null) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaRemoteMediator.kt new file mode 100644 index 000000000..734745f9c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaRemoteMediator.kt @@ -0,0 +1,80 @@ +/* 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.components.account.media + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import com.keylesspalace.tusky.components.timeline.util.ifExpected +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import kotlinx.coroutines.rx3.await +import retrofit2.HttpException + +@OptIn(ExperimentalPagingApi::class) +class AccountMediaRemoteMediator( + private val api: MastodonApi, + private val viewModel: AccountMediaViewModel +) : RemoteMediator() { + + override suspend fun load( + loadType: LoadType, + state: PagingState + ): MediatorResult { + + try { + val statusResponse = when (loadType) { + LoadType.REFRESH -> { + api.accountStatuses(viewModel.accountId, onlyMedia = true).await() + } + LoadType.PREPEND -> { + return MediatorResult.Success(endOfPaginationReached = true) + } + LoadType.APPEND -> { + val maxId = state.lastItemOrNull()?.statusId + if (maxId != null) { + api.accountStatuses(viewModel.accountId, maxId = maxId, onlyMedia = true).await() + } else { + return MediatorResult.Success(endOfPaginationReached = false) + } + } + } + + val statuses = statusResponse.body() + if (!statusResponse.isSuccessful || statuses == null) { + return MediatorResult.Error(HttpException(statusResponse)) + } + + val attachments = statuses.flatMap { status -> + AttachmentViewData.list(status) + } + + if (loadType == LoadType.REFRESH) { + viewModel.attachmentData.clear() + } + + viewModel.attachmentData.addAll(attachments) + + viewModel.currentSource?.invalidate() + return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty()) + } catch (e: Exception) { + return ifExpected(e) { + MediatorResult.Error(e) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaViewModel.kt new file mode 100644 index 000000000..5c3528e9d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaViewModel.kt @@ -0,0 +1,64 @@ +/* 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.components.account.media + +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.network.MastodonApi +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import javax.inject.Inject + +class AccountMediaViewModel @Inject constructor ( + api: MastodonApi +) : ViewModel() { + + lateinit var accountId: String + + val attachmentData: MutableList = mutableListOf() + + var currentSource: AccountMediaPagingSource? = null + + @OptIn(ExperimentalPagingApi::class) + val media = Pager( + config = PagingConfig( + pageSize = LOAD_AT_ONCE, + prefetchDistance = LOAD_AT_ONCE * 2 + ), + pagingSourceFactory = { + AccountMediaPagingSource( + viewModel = this + ).also { source -> + currentSource = source + } + }, + remoteMediator = AccountMediaRemoteMediator(api, this) + ).flow + .cachedIn(viewModelScope) + + fun revealAttachment(viewData: AttachmentViewData) { + val position = attachmentData.indexOfFirst { oldViewData -> oldViewData.id == viewData.id } + attachmentData[position] = viewData.copy(isRevealed = true) + currentSource?.invalidate() + } + + companion object { + private const val LOAD_AT_ONCE = 30 + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/GridSpacingItemDecoration.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/GridSpacingItemDecoration.kt new file mode 100644 index 000000000..34ad159e8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/GridSpacingItemDecoration.kt @@ -0,0 +1,47 @@ +/* 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.components.account.media + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration + +class GridSpacingItemDecoration( + private val spanCount: Int, + private val spacing: Int, + private val topOffset: Int +) : ItemDecoration() { + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + val position = parent.getChildAdapterPosition(view) // item position + if (position < topOffset) return + + val column = (position - topOffset) % spanCount // item column + + outRect.left = column * spacing / spanCount // column * ((1f / spanCount) * spacing) + outRect.right = + spacing - (column + 1) * spacing / spanCount // spacing - (column + 1) * ((1f / spanCount) * spacing) + if (position - topOffset >= spanCount) { + outRect.top = spacing // item top + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/SquareImageView.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/SquareImageView.kt similarity index 92% rename from app/src/main/java/com/keylesspalace/tusky/view/SquareImageView.kt rename to app/src/main/java/com/keylesspalace/tusky/components/account/media/SquareImageView.kt index d7e753bbb..b696bfc11 100644 --- a/app/src/main/java/com/keylesspalace/tusky/view/SquareImageView.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/SquareImageView.kt @@ -1,4 +1,4 @@ -package com.keylesspalace.tusky.view +package com.keylesspalace.tusky.components.account.media import android.content.Context import android.util.AttributeSet 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 05444b5ab..ef6eb2af5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/ViewModelFactory.kt @@ -5,6 +5,7 @@ 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.media.AccountMediaViewModel import com.keylesspalace.tusky.components.announcements.AnnouncementsViewModel import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.conversation.ConversationsViewModel @@ -113,5 +114,10 @@ abstract class ViewModelModule { @IntoMap @ViewModelKey(ViewThreadViewModel::class) internal abstract fun viewThreadViewModel(viewModel: ViewThreadViewModel): ViewModel + + @Binds + @IntoMap + @ViewModelKey(AccountMediaViewModel::class) + internal abstract fun accountMediaViewModel(viewModel: AccountMediaViewModel): ViewModel // Add more ViewModels here } diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index cb07545cd..50d090f43 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -319,12 +319,12 @@ interface MastodonApi { @GET("api/v1/accounts/{id}/statuses") fun accountStatuses( @Path("id") accountId: String, - @Query("max_id") maxId: String?, - @Query("since_id") sinceId: String?, - @Query("limit") limit: Int?, - @Query("exclude_replies") excludeReplies: Boolean?, - @Query("only_media") onlyMedia: Boolean?, - @Query("pinned") pinned: Boolean? + @Query("max_id") maxId: String? = null, + @Query("since_id") sinceId: String? = null, + @Query("limit") limit: Int? = null, + @Query("exclude_replies") excludeReplies: Boolean? = null, + @Query("only_media") onlyMedia: Boolean? = null, + @Query("pinned") pinned: Boolean? = null ): Single>> @GET("api/v1/accounts/{id}/followers") diff --git a/app/src/main/java/com/keylesspalace/tusky/util/AttachmentHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/AttachmentHelper.kt new file mode 100644 index 000000000..6307e7211 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/AttachmentHelper.kt @@ -0,0 +1,26 @@ +@file:JvmName("AttachmentHelper") +package com.keylesspalace.tusky.util + +import android.content.Context +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Attachment +import kotlin.math.roundToInt + +fun Attachment.getFormattedDescription(context: Context): CharSequence { + var duration = "" + if (meta?.duration != null && meta.duration > 0) { + duration = formatDuration(meta.duration.toDouble()) + " " + } + return if (description.isNullOrEmpty()) { + duration + context.getString(R.string.description_post_media_no_description_placeholder) + } else { + duration + description + } +} + +private fun formatDuration(durationInSeconds: Double): String { + val seconds = durationInSeconds.roundToInt() % 60 + val minutes = durationInSeconds.toInt() % 3600 / 60 + val hours = durationInSeconds.toInt() / 3600 + return "%d:%02d:%02d".format(hours, minutes, seconds) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt index b0a8062f6..ae24cebe1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt @@ -1,22 +1,50 @@ +/* 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.viewdata import android.os.Parcelable import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Status +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Parcelize data class AttachmentViewData( val attachment: Attachment, val statusId: String, - val statusUrl: String + val statusUrl: String, + val sensitive: Boolean, + val isRevealed: Boolean ) : Parcelable { + + @IgnoredOnParcel + val id = attachment.id + companion object { @JvmStatic fun list(status: Status): List { val actionable = status.actionableStatus - return actionable.attachments.map { - AttachmentViewData(it, actionable.id, actionable.url!!) + return actionable.attachments.map { attachment -> + AttachmentViewData( + attachment = attachment, + statusId = actionable.id, + statusUrl = actionable.url!!, + sensitive = actionable.sensitive, + isRevealed = !actionable.sensitive + ) } } } diff --git a/app/src/main/res/layout/item_account_media.xml b/app/src/main/res/layout/item_account_media.xml new file mode 100644 index 000000000..a2938b69e --- /dev/null +++ b/app/src/main/res/layout/item_account_media.xml @@ -0,0 +1,18 @@ + + + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 5ec69307a..ea2e744b6 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -53,4 +53,9 @@ 16dp 36dp + + 3dp + + 16dp + From 5bc680ab78ea6a4f8cd1af1a80b769c55914143d Mon Sep 17 00:00:00 2001 From: Prat <616399+pt2121@users.noreply.github.com> Date: Mon, 12 Sep 2022 09:21:00 -0700 Subject: [PATCH 065/142] Refactor Caption Dialog to handle screen rotation (#2626) (#2693) Co-authored-by: Prat T --- .../components/compose/ComposeActivity.kt | 18 +- .../compose/dialog/CaptionDialog.kt | 186 +++++++++++------- 2 files changed, 127 insertions(+), 77 deletions(-) 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 c7e6f4a26..c2e3eba32 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 @@ -68,7 +68,7 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.adapter.EmojiAdapter import com.keylesspalace.tusky.adapter.LocaleAdapter import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener -import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog +import com.keylesspalace.tusky.components.compose.dialog.CaptionDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView @@ -118,7 +118,8 @@ class ComposeActivity : OnEmojiSelectedListener, Injectable, OnReceiveContentListener, - ComposeScheduleView.OnTimeSetListener { + ComposeScheduleView.OnTimeSetListener, + CaptionDialog.Listener { @Inject lateinit var viewModelFactory: ViewModelFactory @@ -213,9 +214,8 @@ class ComposeActivity : val mediaAdapter = MediaPreviewAdapter( this, onAddCaption = { item -> - makeCaptionDialog(item.description, item.uri) { newDescription -> - viewModel.updateDescription(item.localId, newDescription) - } + CaptionDialog.newInstance(item.localId, item.description, item.uri) + .show(supportFragmentManager, "caption_dialog") }, onEditImage = this::editImageInQueue, onRemove = this::removeMediaFromQueue @@ -1149,6 +1149,14 @@ class ComposeActivity : scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN } + override fun onUpdateDescription(localId: Int, description: String) { + lifecycleScope.launch { + if (!viewModel.updateDescription(localId, description)) { + Toast.makeText(this@ComposeActivity, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show() + } + } + } + @Parcelize data class ComposeOptions( // Let's keep fields var until all consumers are Kotlin diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt index 25b1d260d..614b87eed 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt @@ -15,19 +15,22 @@ package com.keylesspalace.tusky.components.compose.dialog -import android.app.Activity -import android.content.DialogInterface +import android.app.Dialog +import android.content.Context import android.graphics.drawable.Drawable import android.net.Uri +import android.os.Bundle import android.text.InputFilter import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup import android.view.WindowManager import android.widget.EditText import android.widget.LinearLayout -import android.widget.Toast import androidx.appcompat.app.AlertDialog -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope +import androidx.core.os.bundleOf +import androidx.fragment.app.DialogFragment import at.connyduck.sparkbutton.helpers.Utils import com.bumptech.glide.Glide import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy @@ -35,85 +38,124 @@ import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition import com.github.chrisbanes.photoview.PhotoView import com.keylesspalace.tusky.R -import kotlinx.coroutines.launch // https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32 private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500 -fun T.makeCaptionDialog( - existingDescription: String?, - previewUri: Uri, - onUpdateDescription: suspend (String) -> Boolean -) where T : Activity, T : LifecycleOwner { - val dialogLayout = LinearLayout(this) - val padding = Utils.dpToPx(this, 8) - dialogLayout.setPadding(padding, padding, padding, padding) +class CaptionDialog : DialogFragment() { - dialogLayout.orientation = LinearLayout.VERTICAL - val imageView = PhotoView(this).apply { - maximumScale = 6f - } + private lateinit var listener: Listener + private lateinit var input: EditText - val margin = Utils.dpToPx(this, 4) - dialogLayout.addView(imageView) - (imageView.layoutParams as LinearLayout.LayoutParams).weight = 1f - imageView.layoutParams.height = 0 - (imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0) + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val context = requireContext() + val dialogLayout = LinearLayout(context) + val padding = Utils.dpToPx(context, 8) + dialogLayout.setPadding(padding, padding, padding, padding) - val input = EditText(this) - input.hint = resources.getQuantityString( - R.plurals.hint_describe_for_visually_impaired, - MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT - ) - dialogLayout.addView(input) - (input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin) - input.setLines(2) - input.inputType = ( - InputType.TYPE_CLASS_TEXT - or InputType.TYPE_TEXT_FLAG_MULTI_LINE - or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES - ) - input.setText(existingDescription) - input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT)) - - val okListener = { dialog: DialogInterface, _: Int -> - lifecycleScope.launch { - if (!onUpdateDescription(input.text.toString())) { - showFailedCaptionMessage() - } + dialogLayout.orientation = LinearLayout.VERTICAL + val imageView = PhotoView(context).apply { + maximumScale = 6f } - dialog.dismiss() + + val margin = Utils.dpToPx(context, 4) + dialogLayout.addView(imageView) + (imageView.layoutParams as LinearLayout.LayoutParams).weight = 1f + imageView.layoutParams.height = 0 + (imageView.layoutParams as LinearLayout.LayoutParams).setMargins(0, margin, 0, 0) + + input = EditText(context) + input.hint = resources.getQuantityString( + R.plurals.hint_describe_for_visually_impaired, + MEDIA_DESCRIPTION_CHARACTER_LIMIT, MEDIA_DESCRIPTION_CHARACTER_LIMIT + ) + dialogLayout.addView(input) + (input.layoutParams as LinearLayout.LayoutParams).setMargins(margin, margin, margin, margin) + input.setLines(2) + input.inputType = ( + InputType.TYPE_CLASS_TEXT + or InputType.TYPE_TEXT_FLAG_MULTI_LINE + or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES + ) + input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT)) + input.setText(arguments?.getString(EXISTING_DESCRIPTION_ARG)) + + val localId = arguments?.getInt(LOCAL_ID_ARG) ?: error("Missing localId") + val dialog = AlertDialog.Builder(context) + .setView(dialogLayout) + .setPositiveButton(android.R.string.ok) { _, _ -> + listener.onUpdateDescription(localId, input.text.toString()) + } + .setNegativeButton(android.R.string.cancel, null) + .create() + + isCancelable = false + val window = dialog.window + window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + + val previewUri = + arguments?.getParcelable(PREVIEW_URI_ARG) ?: error("Preview Uri is null") + // Load the image and manually set it into the ImageView because it doesn't have a fixed size. + Glide.with(this) + .load(previewUri) + .downsample(DownsampleStrategy.CENTER_INSIDE) + .into(object : CustomTarget(4096, 4096) { + override fun onLoadCleared(placeholder: Drawable?) { + imageView.setImageDrawable(placeholder) + } + + override fun onResourceReady( + resource: Drawable, + transition: Transition?, + ) { + imageView.setImageDrawable(resource) + } + }) + + return dialog } - val dialog = AlertDialog.Builder(this) - .setView(dialogLayout) - .setPositiveButton(android.R.string.ok, okListener) - .setNegativeButton(android.R.string.cancel, null) - .setCancelable(false) - .create() + override fun onSaveInstanceState(outState: Bundle) { + outState.putString(DESCRIPTION_KEY, input.text.toString()) + super.onSaveInstanceState(outState) + } - val window = dialog.window - window?.setSoftInputMode( - WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE - ) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + savedInstanceState?.getString(DESCRIPTION_KEY)?.let { + input.setText(it) + } + return super.onCreateView(inflater, container, savedInstanceState) + } - dialog.show() + override fun onAttach(context: Context) { + super.onAttach(context) + listener = context as? Listener ?: error("Activity is not ComposeCaptionDialog.Listener") + } - // Load the image and manually set it into the ImageView because it doesn't have a fixed size. - Glide.with(this) - .load(previewUri) - .downsample(DownsampleStrategy.CENTER_INSIDE) - .into(object : CustomTarget(4096, 4096) { - override fun onLoadCleared(placeholder: Drawable?) { - imageView.setImageDrawable(placeholder) - } + interface Listener { + fun onUpdateDescription(localId: Int, description: String) + } - override fun onResourceReady(resource: Drawable, transition: Transition?) { - imageView.setImageDrawable(resource) - } - }) -} - -private fun Activity.showFailedCaptionMessage() { - Toast.makeText(this, R.string.error_failed_set_caption, Toast.LENGTH_SHORT).show() + companion object { + fun newInstance( + localId: Int, + existingDescription: String?, + previewUri: Uri, + ) = CaptionDialog().apply { + arguments = bundleOf( + LOCAL_ID_ARG to localId, + EXISTING_DESCRIPTION_ARG to existingDescription, + PREVIEW_URI_ARG to previewUri, + ) + } + + private const val DESCRIPTION_KEY = "description" + private const val EXISTING_DESCRIPTION_ARG = "existing_description" + private const val PREVIEW_URI_ARG = "preview_uri" + private const val LOCAL_ID_ARG = "local_id" + } } From 21fa853a475fa274969eaec4dc3e9f452927cfed Mon Sep 17 00:00:00 2001 From: Luca Date: Fri, 9 Sep 2022 09:27:23 +0000 Subject: [PATCH 066/142] Translated using Weblate (Italian) Currently translated at 99.3% (488 of 491 strings) Translated using Weblate (Italian) Currently translated at 99.3% (488 of 491 strings) Co-authored-by: Luca Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/it/ Translation: Tusky/Tusky --- app/src/main/res/values-it/strings.xml | 145 +++++++++++++------------ 1 file changed, 77 insertions(+), 68 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 54da752b5..22da6eb32 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -3,20 +3,20 @@ Si è verificato un errore. Si è verificato un errore di rete! Per favore controlla la tua connessione e riprova! Questo non può essere vuoto. - Inserito dominio non valido + Inserito un dominio non valido Autenticazione con quell\'istanza fallita. Nessun browser web utilizzabile trovato. Si è verificato un errore di autenticazione non identificato. Autorizzazione negata. Acquisizione token di accesso fallita. - Il post è troppo lungo! + Il messaggio è troppo lungo! Quel tipo di file non può essere caricato. Non è stato possibile aprire quel file. È richiesto il permesso di leggere file. È richiesto il permesso di salvare file. - Non è possibile allegare allo stesso post immagini e video. + Non è possibile allegare nello stesso messaggio immagini e video. Il caricamento è fallito. - Errore nell\'invio del post. + Errore nell\'invio del messaggio. Home Notifiche Locale @@ -24,7 +24,7 @@ Messaggi diretti Schede Conversazione - Post + Messaggi Con risposte Fissati Seguiti @@ -48,8 +48,8 @@ Qui non c\'è nulla. Qui non c\'è nulla. Trascina verso il basso per aggiornare! %s ha condiviso il tuo post - %s ha messo il tuo post nei preferiti - %s ti ha seguito + %s ha messo il tuo messaggio nei preferiti + %s ti segue Segnala @%s Commenti aggiuntivi? Risposta veloce @@ -100,7 +100,7 @@ Rifiuta Cerca Bozze - Visibilità dei post + Visibilità dei messaggi Avviso di contenuto sensibile Tastiera emoji Aggiungi scheda @@ -115,11 +115,11 @@ Collegamenti Apri media #%d Scaricando %1$s - Copia link + Copia collegamento Apri come %s Condividi come … - Condividi URL del post su… - Condividi post su… + Condividi URL del messaggio su… + Condividi messaggio su… Condividi media su… Inviato! Utente sbloccato @@ -150,10 +150,10 @@ Scarica Revocare la richiesta di seguire? Smettere di seguire questo account? - Eliminare questo post\? + Eliminare questo messaggio\? Pubblico: visibile sulle timeline pubbliche Non in elenco: non visibile sulle timeline pubbliche - Solo follower: visibile solo dai tuoi follower + Solo chi ti segue: visibile solo da chi ti segue Diretto: visibile solo agli utenti menzionati Notifiche Notifiche @@ -164,8 +164,8 @@ Notificami quando vengo menzionato vengo seguito - i miei post vengono condivisi - i miei post vengono messi nei preferiti + i miei messaggi vengono condivisi + i miei messaggi vengono messi nei preferiti Aspetto Tema dell\'app Timeline @@ -195,8 +195,8 @@ Sincronizzazione delle impostazioni fallita Pubblico Non in elenco - Solo follower - Dimensione del testo dei post + Solo seguaci + Dimensione del testo dei messaggi Piccolissimo Piccolo Normale @@ -204,12 +204,12 @@ Grandissimo Nuove menzioni Notifiche di quando vieni menzionato da qualcuno - Nuovi follower - Notifiche su nuovi follower + Nuovi seguaci + Notifiche su nuovi seguaci Condivisioni - Notifiche sui tuoi post che vengono condivisi + Notifiche sui tuoi messaggi che vengono condivisi Preferiti - Notifiche sui tuoi post che vengono segnati come preferiti + Notifiche sui tuoi messaggi che vengono segnati come preferiti %s ti ha menzionato %1$s, %2$s, %3$s e %4$d altri %1$s, %2$s e %3$s @@ -235,8 +235,8 @@ Segnala problemi e richiedi funzionalità: \n https://github.com/tuskyapp/Tusky/issues Profilo di Tusky - Condividi contenuto del post - Condividi link al post + Condividi contenuto del messaggio + Condividi collegamento al messaggio Immagini Video Richiesta inviata @@ -252,10 +252,10 @@ %dmin %ds Ti segue - Mostra sempre tutti i contenuti sensibili + Mostra sempre i contenuti sensibili Media Rispondendo a @%s - carica altri + carica altro Timeline pubbliche Conversazioni Aggiungi filtro @@ -287,19 +287,19 @@ Blocca account Richiedi una tua approvazione manuale per seguirti Salvare bozza? - Inviando il post… + Inviando il messaggio… Errore durante l\'invio - Invio post + Invio messaggi Invio annullato - Una copia del post è stata salvata nelle tue bozze + Una copia del messaggio è stata salvata nelle tue bozze Componi La tua istanza %s non ha nessuna emoji personalizzata Stile delle emoji Predefinite del sistema Dovrai prima scaricare questo pacchetto di emoji Ricerca in corso… - Espandi/riduci tutti i post - Apri post + Espandi/riduci tutti i messaggi + Apri messaggio Riavvio dell\'app richiesto Devi riavviare Tusky per applicare queste modifiche Più tardi @@ -307,10 +307,10 @@ Le emoji predefinite del tuo dispositivo Le emoji Blob di Android 4.4-7.1 Le emoji standard di Mastodon - Download fallito + Scaricamento fallito Bot %1$s si è spostato su: - Condividi con la visibilità del post originale + Condividi con la visibilità del messaggio originale Annulla condivisione Tusky contiene codice e risorse dai seguenti progetti open source: Licenziata sotto la Licenza Apache (copia sotto) @@ -329,8 +329,8 @@ %1$s Preferiti - <b>%s</b> Boost - <b>%s</b> Boost + %s Condivisione + %s Condivisioni Condiviso da Aggiunto ai preferiti da @@ -342,23 +342,20 @@ limite massimo di %1$d schede raggiunto Media: %s - Contenuto sensibile: %s - - Nessuna descrizione - - Ribloggato - + Contenuto sensibile: %s + Nessuna descrizione + Ribloggato Messo nei preferiti Pubblico Non in elenco - Solo follower + Solo seguaci Diretti Nome della lista Scarica media Scaricando media - Componi post + Componi messaggio Hashtag senza # Componi Svuota @@ -367,7 +364,7 @@ Mostra indicatore bot Sei sicuro di voler permanentemente eliminare tutte le tue notifiche\? Cancella e riscrivi - Cancellare e riscrivere questo post\? + Cancellare e riscrivere questo messaggio\? %s voto %s voti @@ -382,9 +379,9 @@ Sei sicuro di voler bloccare tutto %s\? Non vedrai nessun contenuto da quel dominio in nessuna timeline pubblica o nelle tue notifiche. I tuoi seguaci che stanno in quel dominio saranno rimossi. Nascondi l\'intero dominio dei sondaggi si sono conclusi - Riproduci animazioni avatar + Riproduci animazioni avatars Votazioni - Notifiche sulle votazioni che si sono concluse + Notifiche sui sondaggi che si sono conclusi Parola intera Quando la parola chiave o la frase sono composte da soli caratteri alfanumerici, sarà applicata solo se corrisponde alla parola completa Set di emoji di Google @@ -394,7 +391,7 @@ Segnalibri Aggiungi sondaggio Fatto usando Tusky - Espandi sempre i post segnalati come contenuto sensibile + Espandi sempre i messaggi segnalati come contenuto sensibile Messo nei segnalibri Sondaggio con scelte: %1$s, %2$s, %3$s, %4$s; %5$s Scegli lista @@ -408,7 +405,7 @@ %d ora rimasta - %d ore rimasti + %d ore rimaste %d minuto rimasto @@ -425,8 +422,8 @@ Altri commenti Inoltra a %s Segnalazione fallita - Scaricamento dei post fallito - La segnalazione sarà inviata al moderatore del tuo server. Puoi spiegare perchè stai segnalando questo utente qui sotto: + Scaricamento dei messaggio fallito + La segnalazione sarà inviata al moderatore del tuo server. Puoi spiegare perchè stai segnalando l\'utente qui sotto: L\'utente è su un altro server. Mandare una copia della segnalazione anche lì\? Utenti Errore durante la ricerca @@ -445,8 +442,8 @@ Modifica Errore nella ricerca del post %s Post programmati - Post programmati - Programma un post + Messaggi programmati + Programma un messaggio Ripristina %1$s • %2$s Non hai bozze. @@ -467,11 +464,11 @@ Salvato! La tua nota privata su questo account Nascondi il titolo della barra degli strumenti in alto - Mostra la finestra di conferma prima di condividere + Chiedi conferma prima di condividere Mostra le anteprime dei collegamenti nelle timelines Mastodon ha un intervallo di programmazione minimo di 5 minuti. Non ci sono annunci. - Non hai post pianificati. + Non hai messaggi programmati. Abilita il gesto di scorrimento per passare da una scheda all\'altra Notifiche sulle richieste di essere seguiti In fondo @@ -485,20 +482,20 @@ mi viene richiesto di seguirmi Nascondi statistiche quantitative sui profili Nascondi le statistiche quantitative sui post - Limita notifiche riguardo statistiche quantitative + Limita notifiche (esclude condivisioni, preferiti e nuovi seguaci) Rivedi le notifiche Benessere - Notifiche di nuovi post di qualcuno a cui sei iscritto - Nuovi post - qualcuno che seguo ha pubblicato un nuovo post + Notifiche di nuovi messaggi di qualcuno a cui sei iscritto + Nuovi messaggi + qualcuno che seguo ha pubblicato un nuovo messaggio %s ha appena pubblicato Non puoi caricare più di %1$d allegato multimediale. Non puoi caricare più di %1$d allegati multimediali. - Il post a cui hai scritto una risposta è stato rimosso + Il messaggio a cui hai scritto una bozza di risposta è stato rimosso Bozza eliminata - L\'invio di questo post è fallito! + L\'invio di questo messaggio è fallito! Sei sicuro di voler cancellare la lista %s\? Indefinita Durata @@ -518,27 +515,39 @@ \n \n Le notifiche push non saranno influenzate, ma puoi modificare le tue impostazioni delle notifiche manualmente. Rimuovi segnalibro - Chiedi conferma prima di condividere + Chiedi conferma prima di apprezzare 14 giorni 30 giorni 60 giorni 90 giorni 180 giorni 365 giorni - Anche se il tuo account non è bloccato, lo staff di %1$s ha pensato che potresti voler verificare le richieste di seguirti da parte questi account manualmente. + Anche se il tuo account non è bloccato, lo staff di %1$s ha pensato che potresti voler verificare le richieste di seguirti da parte questi utenti manualmente. %s si è registrato qualcuno si è registrato - Login - %s ha modificato il suo post - un post con cui ho interagito è stato modificato - Componi post + Accesso + %s ha modificato il suo messaggio + un messaggio con cui ho interagito è stato modificato + Componi messaggio Registrazioni Notifiche di quando qualcuno si è registrato - Modifiche ai post - Notifiche di quando i post con cui hai interagito vengono modificati - Non è stato possibile caricare la pagina di login. + Modifiche ai messaggi + Notifiche di quando i messaggi con cui hai interagito vengono modificati + Non è stato possibile caricare la pagina di accesso. Modifica immagine Salvataggio bozza… Scartare Dettagli + Riaccedi a tutti le utenze per attivare il supporto delle notifiche. + Al fine di utilizzare le notifiche tramite UnifiedPush, Tusky ha bisogno del permesso di sottoscrivere alle notifiche nella tua istanza Mastodon. Questo richiede un nuovo accesso per cambiare l\'OAuth precedentemente concesso a Tusky. Usare questa opzione qui o nelle preferenze dell\'account preserva tutte le tue bozze locali e la memoria temporanea (cache). + %s (%s) + Nuovo accesso eseguito per l\'utenza corrente al fine di garantire il permesso delle notifiche a Tusky. Però hai altre utenze che non sono state migrate in questo modo. Cambia utenza e riaccedi una alla volta per abilitare il supporto alle notifiche UnifiedPush. + Registrato da %1$s + Video and audio files non possono eccedere %s MB in dimensione. + L\'immagine non può essere modificata. + Riaccedi per le notifiche + Errore provando a seguire #%s + Errore smettendo di provare a seguire #%s + 1+ + Caricamento dettagli utente fallito \ No newline at end of file From deeb0678f68bca795f010aaa5becf9edf0ac4cdc Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Fri, 9 Sep 2022 09:27:23 +0000 Subject: [PATCH 067/142] Translated using Weblate (Italian) Currently translated at 99.3% (488 of 491 strings) Co-authored-by: Stefano Pigozzi Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/it/ Translation: Tusky/Tusky --- app/src/main/res/values-it/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 22da6eb32..39e154e07 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -482,7 +482,7 @@ mi viene richiesto di seguirmi Nascondi statistiche quantitative sui profili Nascondi le statistiche quantitative sui post - Limita notifiche (esclude condivisioni, preferiti e nuovi seguaci) + Nascondi notifiche quantitative Rivedi le notifiche Benessere Notifiche di nuovi messaggi di qualcuno a cui sei iscritto From 00024864ce44d407dd969d1a68cffc8f93149f33 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 9 Sep 2022 09:27:23 +0000 Subject: [PATCH 068/142] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (492 of 492 strings) Co-authored-by: Eric Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/ Translation: Tusky/Tusky --- app/src/main/res/values-zh-rCN/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index cd316f0b2..802ba84a8 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -549,4 +549,5 @@ 取关 #%s 出错 %s (%s) (无更改) + 帖子语言 \ No newline at end of file From 8336b4ddabccd3cd558e619edbe1bfbaa9641d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Fri, 9 Sep 2022 09:27:23 +0000 Subject: [PATCH 069/142] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (492 of 492 strings) Co-authored-by: Hồ Nhất Duy Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ Translation: Tusky/Tusky --- app/src/main/res/values-vi/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 5e1f19b44..5f412a315 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -530,4 +530,5 @@ Lỗi khi bỏ theo dõi #%s %s (%s) (Không đổi) + Ngôn ngữ đăng \ No newline at end of file From 0dac54ae26300d596106bfcd52806863ae0b7808 Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Fri, 9 Sep 2022 09:27:23 +0000 Subject: [PATCH 070/142] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (492 of 492 strings) Co-authored-by: Vegard Skjefstad Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nb_NO/ Translation: Tusky/Tusky --- app/src/main/res/values-nb-rNO/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-nb-rNO/strings.xml b/app/src/main/res/values-nb-rNO/strings.xml index 529b1dec1..f27f026cd 100644 --- a/app/src/main/res/values-nb-rNO/strings.xml +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -541,4 +541,5 @@ Det oppsto en feil når følging av #%s skulle avsluttes %s (%s) (Ingen endring) + Innleggspråk \ No newline at end of file From b3532c07494399156a0be6750a94ffe4dc98e1ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Fri, 9 Sep 2022 09:27:23 +0000 Subject: [PATCH 071/142] Translated using Weblate (Icelandic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (492 of 492 strings) Co-authored-by: Sveinn í Felli Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/is/ Translation: Tusky/Tusky --- app/src/main/res/values-is/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 28244b188..e6105c0fc 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -534,4 +534,12 @@ Skráðu aftur inn fyrir ýti-tilkynningar Hunsa Nánar + %s (%s) + Myndskeiða- og hljóðskrár geta ekki verið stærri en %s MB. + Tungumál færslu + (engin breyting) + Villa við að fylgjast með #%s + 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. \ No newline at end of file From 655ce30031bc274864ac942833303d3ca13b1aa3 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 13 Sep 2022 19:47:55 +0200 Subject: [PATCH 072/142] migrate timeline api calls from Rx Single to suspending functions (#2690) * migrate timeline api calls from Rx Single to suspending functions * fix tests --- .../media/AccountMediaRemoteMediator.kt | 5 +- .../viewmodel/CachedTimelineRemoteMediator.kt | 7 +- .../viewmodel/CachedTimelineViewModel.kt | 3 +- .../viewmodel/NetworkTimelineViewModel.kt | 3 +- .../tusky/network/MastodonApi.kt | 29 ++-- .../CachedTimelineRemoteMediatorTest.kt | 139 ++++++++---------- 6 files changed, 80 insertions(+), 106 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaRemoteMediator.kt index 734745f9c..81865b0fe 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaRemoteMediator.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaRemoteMediator.kt @@ -22,7 +22,6 @@ import androidx.paging.RemoteMediator import com.keylesspalace.tusky.components.timeline.util.ifExpected import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.viewdata.AttachmentViewData -import kotlinx.coroutines.rx3.await import retrofit2.HttpException @OptIn(ExperimentalPagingApi::class) @@ -39,7 +38,7 @@ class AccountMediaRemoteMediator( try { val statusResponse = when (loadType) { LoadType.REFRESH -> { - api.accountStatuses(viewModel.accountId, onlyMedia = true).await() + api.accountStatuses(viewModel.accountId, onlyMedia = true) } LoadType.PREPEND -> { return MediatorResult.Success(endOfPaginationReached = true) @@ -47,7 +46,7 @@ class AccountMediaRemoteMediator( LoadType.APPEND -> { val maxId = state.lastItemOrNull()?.statusId if (maxId != null) { - api.accountStatuses(viewModel.accountId, maxId = maxId, onlyMedia = true).await() + api.accountStatuses(viewModel.accountId, maxId = maxId, onlyMedia = true) } else { return MediatorResult.Success(endOfPaginationReached = false) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt index ebab4440a..b29ab0cf6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt @@ -30,7 +30,6 @@ import com.keylesspalace.tusky.db.TimelineStatusEntity import com.keylesspalace.tusky.db.TimelineStatusWithAccount import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi -import kotlinx.coroutines.rx3.await import retrofit2.HttpException @OptIn(ExperimentalPagingApi::class) @@ -71,7 +70,7 @@ class CachedTimelineRemoteMediator( maxId = cachedTopId, sinceId = topPlaceholderId, // so already existing placeholders don't get accidentally overwritten limit = state.config.pageSize - ).await() + ) val statuses = statusResponse.body() if (statusResponse.isSuccessful && statuses != null) { @@ -86,14 +85,14 @@ class CachedTimelineRemoteMediator( val statusResponse = when (loadType) { LoadType.REFRESH -> { - api.homeTimeline(sinceId = topPlaceholderId, limit = state.config.pageSize).await() + api.homeTimeline(sinceId = topPlaceholderId, limit = state.config.pageSize) } LoadType.PREPEND -> { return MediatorResult.Success(endOfPaginationReached = true) } LoadType.APPEND -> { val maxId = state.pages.findLast { it.data.isNotEmpty() }?.data?.lastOrNull()?.status?.serverId - api.homeTimeline(maxId = maxId, limit = state.config.pageSize).await() + api.homeTimeline(maxId = maxId, limit = state.config.pageSize) } } 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 97bc625b4..217055dc0 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 @@ -50,7 +50,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.coroutines.rx3.await import retrofit2.HttpException import javax.inject.Inject import kotlin.time.DurationUnit @@ -176,7 +175,7 @@ class CachedTimelineViewModel @Inject constructor( sinceId = nextPlaceholderId, limit = LOAD_AT_ONCE ) - }.await() + } val statuses = response.body() if (!response.isSuccessful || statuses == null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt index 8c81df1db..7335e8f36 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt @@ -45,7 +45,6 @@ import kotlinx.coroutines.asExecutor import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -import kotlinx.coroutines.rx3.await import retrofit2.HttpException import retrofit2.Response import java.io.IOException @@ -298,7 +297,7 @@ class NetworkTimelineViewModel @Inject constructor( Kind.FAVOURITES -> api.favourites(fromId, uptoId, limit) Kind.BOOKMARKS -> api.bookmarks(fromId, uptoId, limit) Kind.LIST -> api.listTimeline(id!!, fromId, uptoId, limit) - }.await() + } } private fun StatusViewData.Concrete.update() { 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 50d090f43..ca322031e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -85,37 +85,38 @@ interface MastodonApi { fun getFilters(): Single> @GET("api/v1/timelines/home") - fun homeTimeline( + @Throws(Exception::class) + suspend fun homeTimeline( @Query("max_id") maxId: String? = null, @Query("since_id") sinceId: String? = null, @Query("limit") limit: Int? = null - ): Single>> + ): Response> @GET("api/v1/timelines/public") - fun publicTimeline( + suspend fun publicTimeline( @Query("local") local: Boolean? = null, @Query("max_id") maxId: String? = null, @Query("since_id") sinceId: String? = null, @Query("limit") limit: Int? = null - ): Single>> + ): Response> @GET("api/v1/timelines/tag/{hashtag}") - fun hashtagTimeline( + suspend fun hashtagTimeline( @Path("hashtag") hashtag: String, @Query("any[]") any: List?, @Query("local") local: Boolean?, @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Single>> + ): Response> @GET("api/v1/timelines/list/{listId}") - fun listTimeline( + suspend fun listTimeline( @Path("listId") listId: String, @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Single>> + ): Response> @GET("api/v1/notifications") fun notifications( @@ -317,7 +318,7 @@ interface MastodonApi { * @param onlyMedia only return statuses that have media attached */ @GET("api/v1/accounts/{id}/statuses") - fun accountStatuses( + suspend fun accountStatuses( @Path("id") accountId: String, @Query("max_id") maxId: String? = null, @Query("since_id") sinceId: String? = null, @@ -325,7 +326,7 @@ interface MastodonApi { @Query("exclude_replies") excludeReplies: Boolean? = null, @Query("only_media") onlyMedia: Boolean? = null, @Query("pinned") pinned: Boolean? = null - ): Single>> + ): Response> @GET("api/v1/accounts/{id}/followers") fun accountFollowers( @@ -419,18 +420,18 @@ interface MastodonApi { fun unblockDomain(@Field("domain") domain: String): Call @GET("api/v1/favourites") - fun favourites( + suspend fun favourites( @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Single>> + ): Response> @GET("api/v1/bookmarks") - fun bookmarks( + suspend fun bookmarks( @Query("max_id") maxId: String?, @Query("since_id") sinceId: String?, @Query("limit") limit: Int? - ): Single>> + ): Response> @GET("api/v1/follow_requests") fun followRequests( diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt index c117cf59e..927495cfc 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt @@ -17,7 +17,6 @@ import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.Converters import com.keylesspalace.tusky.db.TimelineStatusWithAccount -import io.reactivex.rxjava3.core.Single import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking import okhttp3.ResponseBody.Companion.toResponseBody @@ -30,6 +29,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow import org.mockito.kotlin.mock import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config @@ -78,7 +78,7 @@ class CachedTimelineRemoteMediatorTest { val remoteMediator = CachedTimelineRemoteMediator( accountManager = accountManager, api = mock { - on { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Single.just(Response.error(500, "".toResponseBody())) + onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody()) }, db = db, gson = Gson() @@ -98,7 +98,7 @@ class CachedTimelineRemoteMediatorTest { val remoteMediator = CachedTimelineRemoteMediator( accountManager = accountManager, api = mock { - on { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Single.error(IOException()) + onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException() }, db = db, gson = Gson() @@ -154,22 +154,18 @@ class CachedTimelineRemoteMediatorTest { val remoteMediator = CachedTimelineRemoteMediator( accountManager = accountManager, api = mock { - on { homeTimeline(limit = 3) } doReturn Single.just( - Response.success( - listOf( - mockStatus("8"), - mockStatus("7"), - mockStatus("5") - ) + onBlocking { homeTimeline(limit = 3) } doReturn Response.success( + listOf( + mockStatus("8"), + mockStatus("7"), + mockStatus("5") ) ) - on { homeTimeline(maxId = "3", limit = 3) } doReturn Single.just( - Response.success( - listOf( - mockStatus("3"), - mockStatus("2"), - mockStatus("1") - ) + onBlocking { homeTimeline(maxId = "3", limit = 3) } doReturn Response.success( + listOf( + mockStatus("3"), + mockStatus("2"), + mockStatus("1") ) ) }, @@ -222,22 +218,18 @@ class CachedTimelineRemoteMediatorTest { val remoteMediator = CachedTimelineRemoteMediator( accountManager = accountManager, api = mock { - on { homeTimeline(limit = 20) } doReturn Single.just( - Response.success( - listOf( - mockStatus("8"), - mockStatus("7"), - mockStatus("5") - ) + onBlocking { homeTimeline(limit = 20) } doReturn Response.success( + listOf( + mockStatus("8"), + mockStatus("7"), + mockStatus("5") ) ) - on { homeTimeline(maxId = "3", limit = 20) } doReturn Single.just( - Response.success( - listOf( - mockStatus("3"), - mockStatus("2"), - mockStatus("1") - ) + onBlocking { homeTimeline(maxId = "3", limit = 20) } doReturn Response.success( + listOf( + mockStatus("3"), + mockStatus("2"), + mockStatus("1") ) ) }, @@ -287,22 +279,18 @@ class CachedTimelineRemoteMediatorTest { val remoteMediator = CachedTimelineRemoteMediator( accountManager = accountManager, api = mock { - on { homeTimeline(limit = 3) } doReturn Single.just( - Response.success( - listOf( - mockStatus("6"), - mockStatus("4"), - mockStatus("3") - ) + onBlocking { homeTimeline(limit = 3) } doReturn Response.success( + listOf( + mockStatus("6"), + mockStatus("4"), + mockStatus("3") ) ) - on { homeTimeline(maxId = "3", limit = 3) } doReturn Single.just( - Response.success( - listOf( - mockStatus("3"), - mockStatus("2"), - mockStatus("1") - ) + onBlocking { homeTimeline(maxId = "3", limit = 3) } doReturn Response.success( + listOf( + mockStatus("3"), + mockStatus("2"), + mockStatus("1") ) ) }, @@ -344,13 +332,11 @@ class CachedTimelineRemoteMediatorTest { val remoteMediator = CachedTimelineRemoteMediator( accountManager = accountManager, api = mock { - on { homeTimeline(limit = 20) } doReturn Single.just( - Response.success( - listOf( - mockStatus("5"), - mockStatus("4"), - mockStatus("3") - ) + onBlocking { homeTimeline(limit = 20) } doReturn Response.success( + listOf( + mockStatus("5"), + mockStatus("4"), + mockStatus("3") ) ) }, @@ -397,15 +383,12 @@ class CachedTimelineRemoteMediatorTest { val remoteMediator = CachedTimelineRemoteMediator( accountManager = accountManager, api = mock { - on { homeTimeline(limit = 20) } doReturn Single.just( - Response.success(emptyList()) - ) - on { homeTimeline(maxId = "3", limit = 20) } doReturn Single.just( - Response.success( - listOf( - mockStatus("3"), - mockStatus("1") - ) + onBlocking { homeTimeline(limit = 20) } doReturn Response.success(emptyList()) + + onBlocking { homeTimeline(maxId = "3", limit = 20) } doReturn Response.success( + listOf( + mockStatus("3"), + mockStatus("1") ) ) }, @@ -452,21 +435,17 @@ class CachedTimelineRemoteMediatorTest { val remoteMediator = CachedTimelineRemoteMediator( accountManager = accountManager, api = mock { - on { homeTimeline(sinceId = "6", limit = 20) } doReturn Single.just( - Response.success( - listOf( - mockStatus("9"), - mockStatus("8"), - mockStatus("7") - ) + onBlocking { homeTimeline(sinceId = "6", limit = 20) } doReturn Response.success( + listOf( + mockStatus("9"), + mockStatus("8"), + mockStatus("7") ) ) - on { homeTimeline(maxId = "8", sinceId = "6", limit = 20) } doReturn Single.just( - Response.success( - listOf( - mockStatus("8"), - mockStatus("7") - ) + onBlocking { homeTimeline(maxId = "8", sinceId = "6", limit = 20) } doReturn Response.success( + listOf( + mockStatus("8"), + mockStatus("7") ) ) }, @@ -515,13 +494,11 @@ class CachedTimelineRemoteMediatorTest { val remoteMediator = CachedTimelineRemoteMediator( accountManager = accountManager, api = mock { - on { homeTimeline(maxId = "5", limit = 20) } doReturn Single.just( - Response.success( - listOf( - mockStatus("3"), - mockStatus("2"), - mockStatus("1") - ) + onBlocking { homeTimeline(maxId = "5", limit = 20) } doReturn Response.success( + listOf( + mockStatus("3"), + mockStatus("2"), + mockStatus("1") ) ) }, From eb167c623b9f29a847eba69ef0f4c5afdc007b0b Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 13 Sep 2022 19:48:09 +0200 Subject: [PATCH 073/142] new app icon (#2695) * new app icon * make icon in readme a bit smaller --- README.md | 2 +- .../res/drawable/ic_launcher_background.xml | 11 - app/src/blue/res/mipmap-hdpi/ic_launcher.png | Bin 5913 -> 4696 bytes app/src/blue/res/mipmap-mdpi/ic_launcher.png | Bin 3373 -> 2945 bytes app/src/blue/res/mipmap-xhdpi/ic_launcher.png | Bin 8620 -> 6583 bytes .../blue/res/mipmap-xxhdpi/ic_launcher.png | Bin 14265 -> 10355 bytes .../blue/res/mipmap-xxxhdpi/ic_launcher.png | Bin 20394 -> 14698 bytes .../res/drawable/ic_launcher_background.xml | 11 - app/src/green/res/mipmap-hdpi/ic_launcher.png | Bin 5689 -> 4717 bytes app/src/green/res/mipmap-mdpi/ic_launcher.png | Bin 3385 -> 2941 bytes .../green/res/mipmap-xhdpi/ic_launcher.png | Bin 8407 -> 6591 bytes .../green/res/mipmap-xxhdpi/ic_launcher.png | Bin 14568 -> 10388 bytes .../green/res/mipmap-xxxhdpi/ic_launcher.png | Bin 20911 -> 14762 bytes app/src/green/res/values/flavor-colors.xml | 2 + app/src/main/AndroidManifest.xml | 2 +- app/src/main/ic_launcher-web.png | Bin 71995 -> 27414 bytes app/src/main/ic_launcher.svg | 488 ------------------ app/src/main/res/drawable-hdpi/ic_notify.png | Bin 675 -> 0 bytes app/src/main/res/drawable-mdpi/ic_notify.png | Bin 453 -> 0 bytes .../drawable-v24/ic_launcher_foreground.xml | 14 - app/src/main/res/drawable-xhdpi/ic_notify.png | Bin 888 -> 0 bytes .../main/res/drawable-xxhdpi/ic_notify.png | Bin 1343 -> 0 bytes .../res/drawable/ic_launcher_foreground.xml | 41 +- .../res/drawable/ic_launcher_monochrome.xml | 11 +- app/src/main/res/drawable/ic_notify.xml | 9 + .../main/res/drawable/ic_quicksettings.xml | 6 + app/src/main/res/drawable/ic_splash.xml | 6 +- app/src/main/res/drawable/ic_tusky.xml | 11 - .../res/mipmap-anydpi-v26/ic_launcher.xml | 8 +- app/src/main/res/values/colors.xml | 3 + assets/splash.xcf | Bin 114811 -> 0 bytes assets/tusky_banner.xcf | Bin 3120971 -> 1730949 bytes assets/tusky_logo_borderless.png | Bin 36521 -> 0 bytes .../android/en-US/images/featureGraphic.png | Bin 747700 -> 689428 bytes .../metadata/android/en-US/images/icon.png | Bin 11172 -> 14698 bytes 35 files changed, 58 insertions(+), 567 deletions(-) delete mode 100644 app/src/blue/res/drawable/ic_launcher_background.xml delete mode 100644 app/src/green/res/drawable/ic_launcher_background.xml delete mode 100644 app/src/main/ic_launcher.svg delete mode 100644 app/src/main/res/drawable-hdpi/ic_notify.png delete mode 100644 app/src/main/res/drawable-mdpi/ic_notify.png delete mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml delete mode 100644 app/src/main/res/drawable-xhdpi/ic_notify.png delete mode 100644 app/src/main/res/drawable-xxhdpi/ic_notify.png create mode 100644 app/src/main/res/drawable/ic_notify.xml create mode 100644 app/src/main/res/drawable/ic_quicksettings.xml delete mode 100644 app/src/main/res/drawable/ic_tusky.xml delete mode 100644 assets/splash.xcf delete mode 100644 assets/tusky_logo_borderless.png diff --git a/README.md b/README.md index 6a2840a0b..2ad67003f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![Translate - with Weblate](https://img.shields.io/badge/translate%20with-Weblate-green.svg?style=flat)](https://weblate.tusky.app/) [![OpenCollective](https://opencollective.com/tusky/backers/badge.svg)](https://opencollective.com/tusky/) [![Build Status](https://app.bitrise.io/app/a3e773c3c57a894c/status.svg?token=qLu_Ti4Gp2LWcYT4eo2INQ&branch=develop)](https://app.bitrise.io/app/a3e773c3c57a894c) # Tusky -![](/fastlane/metadata/android/en-US/images/icon.png) + Tusky is a beautiful Android client for [Mastodon](https://github.com/mastodon/mastodon). Mastodon is an ActivityPub federated social network. That means no single entity controls the whole network, rather, like e-mail, volunteers and organisations operate their own independent servers, users from which can all interact with each other seamlessly. diff --git a/app/src/blue/res/drawable/ic_launcher_background.xml b/app/src/blue/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 29d74ed8f..000000000 --- a/app/src/blue/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/blue/res/mipmap-hdpi/ic_launcher.png b/app/src/blue/res/mipmap-hdpi/ic_launcher.png index 2a27b8c08903284be28bf7e7ea10d35699dfd2b1..023b25f7a55015906c8a8b7f63ecc93c651c69a0 100644 GIT binary patch literal 4696 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 diff --git a/app/src/blue/res/mipmap-mdpi/ic_launcher.png b/app/src/blue/res/mipmap-mdpi/ic_launcher.png index cfd10bbdf9b9a07e613d55ced1ecee4e108ee3c5..3463d7bb46f1f11bde9b62ea89ebe0ef577a0e4d 100644 GIT binary patch delta 2939 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 delta 3371 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 literal 8620 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 bE&%>N?`HJTMzXd600000NkvXXu0mjf&;$e| diff --git a/app/src/blue/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/blue/res/mipmap-xxxhdpi/ic_launcher.png index c11643c51a25c9d7f32d560095ceacdb9bf3ca0f..88795199d44608d654479003f16e597295acbbaf 100644 GIT binary patch literal 14698 zcmY*=Q-CH-uYYp=;ha)pq6_IF$g(XB$hqsmLN4HT>lF~32$mMXZ7`pf&+zw`(O6#0A?aDfK!qzHM z65$5Frz(kyxU!Uy*E;XU&^(Qbng_Sfe}CtO12@>j5q`!jgh63xwwNZA9v<=a#7=G? z)e8opq!pRCAq+Y}YcaH}3b(eb>hs|_k&F5r`|}!MT1m@56ZM%g9Q1?Z z3(kL#iSx<&cJsozEyZOT*9~DaY8&NQonO8_Ifu+;epnpHh|@C}IfDk1V9L%ub_)NT zjh81i@qu*M8+At-^eDuQ-GN~f<(Ypxhm6@enu)g%FGuNhby&KJCYT;QH10tgb;q4->Y^z^7qgJnn*Ij^`-!4Tnx1uL5wln`w#9k>Vc2Q}lz@?UXDLZCBE; z`|=?I<)&TA zJ?6K6ThiT?#m_6n>oRcTB@B+J<%O29Qz!duyw+rRd^>qX?UsSE$ZJJPnsTxlBC3&N zvewX!DAyy~2|bkq?dq)&^vkj@OAIKSiOHDbs0`YKd;~_#(l2*}xn#R6$?D*@*-7av zbQoK_73KvwN7j%^Yr&_I9&@zZiGRgC$?`XGWU**7WOfSZ=E#3#?x`!cX42b{@n#H$ zuJ=x*$4qrp5CXC$&ZNAocPH{($}BhXVx(W8y&lG-n5C1+zPUJ{j?g-F?Gl;8i zE_OMh8j{~asz`0jwcJHcWnA;HR?9o`oRuUTh4MHRgfKjU-*y)?@GHhV`P(e$h)8Wn zUWT@a;G{^moz66Xi{ajSmM$R@GZU2Hy|l=GKnG@qINhJvTH*7N+M8?%%>7QrepFHr zk*J=8-us@P&WaRfjgy{*Or0MYuCAR0r$@!Q?fy+?Pit<~=$D#6P?*-B;;IVTMJM(V z+ajK+a|WZA#ZWBq86I8Ur1GT{`SKQi4ePKN-g+-E_cYvHFbl3m$lRGEKTJ23uvTh{ zH;|-05xe+LGMRK`$gR{jpr>w0qJvY}w2=QYT&n4|HWmBP)w$G4$&0;B{@XUOxM!jG zP8ruxP**#AVqGorrK5dcI+-~t;r4{T*>lj8#v(F2(bf!^dgEQl(8}P~L?l|@%ao;6( zk}-?mn+LgJmVg>i_Q)m0W)k$}Lb$iG=nvxqp|xG^CJ4`DO5(?Q?fhH6yPoCY3`scB zt4KxNiM%k5Eu;b|Qfgt*b)J&CB0*1rb7k$wBpv;Kr$kE$Z@R?2cGsO8(gmUkr{9bS z`b$ZVEjg?-WWz@CM9j{RwpIQz2czgj6X47LauDB{i9dHr@@0rMYtN08$@k1vRJ({- z3M@%J3zB-*gC5Hf6TYIjXgz7qee18k>!kZ6zOYvpM`-OJizU1&Sc-azr5y%wTYd#) zfk4}R0Esq${8Wed*+H$>5_XaT_7Hl4S5j+|cA*B+HAHG zNyLY+sCinuOOVW#AvZ2OV$XR_tyC_HJLOg4?&ml8CG?kQ25Ami^Juji-n9cNjnD)` zNnPj-)R*9YnS>`Bu0!se9Mtmqh*ukvcwXL~53P2FnjCl-&LD~Vka^{Hi3!7h3a%gZ zN`0JM?D7pnGy=cj_70}_S;NPi!^x}qlTqRwf2$JrUfrkz^bwBYJCa)BQ_?l0N^Nr! zufo1$I#B(ju%|Xlxz!`k%N5GUYDH&FdKo6K7-cZ!ZsU%l(K$ zxrZ+YSauf14VXOQbcL|#IHkDlzIgEJ{WXv{!oR#dZP(qFaVqYs@>e2oisnC19=aN8 zwV)Wu7!mXyYGwnl;0|=l`IQ^8#o^u>j$Il@{X7YiXNkJXT(h6h*n25ahk0okU6f+17)KJt%g>Rm@jHKKNE?*R(7@QLcS(2swy3N*6eKk9S;5Vbo(e{8 zncyP%?5IbT@L@KWJitH`+=Aq;%9C2)LZhnQd%#;@eGNLp#&xBzHCG~7_Ebl7x)Cs& zp@a|Cs{p#~Qv-hiG)N_nrPe`V8T|sR7kSvPhH9?pyBe(%HLL z#^Q%}+Hssq9i%OCoc&$e0WLB4GFY)+oj>^dIYQw}wbiRb>`H<{(mv z*@6>{_-B0O=BGkI&lEPMg_EQcwL!D#s<^7BQ+f0Gw}Cqd-U45gouxee{s45L6o^LV zz#tBFsfQWmqF!!8v5&&1pgOj$ISKs`8bzhlWy%Da6JsK2M`eb~@E_^B2gzU$Eh>xS z;`KGIKP|l8*mro5+Zo*Zl)jq?ybISZyqC#_2y1!&N1!%}9{dJRr<_aAadeH6E%C9K zYeb|>DLWZ$p=J5|juRrlQ3A6e9P;yc3eehwgO(^ua7`oyR}S}_VE4v}A(uoz)*Y)e z3mKf{N?}LsDxaP9sOspaj5j&&cusMMj8*72SF(_9`a;&SM5qR-tSSj9^Rl#QqRiqT zn^_WH23UF+-y}^i*ctARxWP}C0an*-%pf>vJ0wwmq7{HJhp`Iv%I&D;#wX#T@o98m zaHFRg_6VU$DfV{Tykn}AR?9g?6gAsM`b|eR(AVDC&vY3o7A2P(t$&ng7UQ)vI$p-t zB6x_9SA=y?%i$Sz=8-q^@>uk`AXBul?T?0n1^qAAP0N)QLY6pNe06wrh8tQ7#Y zpDMx@0egnOsH0>sAX14NCJlVbb7!u+mtj9oMmB$MUO7B~LWZ@7(VTP9$tGLF=_*XoV)TTViQ;;I7?UD+ zt`d!2D}Nudrd!$!a6XKEdJu)7dpmU`WJHU)oU`aUynEz_OIlBkOBE?mQ*3@z)L0Em zEh88JnS*aamiW+d89dqBW?nWIE39AY_#J5TiGZ^gYq^g>6buoA*&3~9escVx*<$Rf zu{MzpwGn0paVJ4udZiPQ(LKecQ&ZN@aI`#ieD`3><$#{+>Whg+aWRB?|UnX!Gs z#_breDN=mW=lRX*inWNo$r=*E$wW*_{qq6iMKiEn>bgH?;mwSm^eb8_)AV>lTd>%E z+B3n9o*~n-ur)8V=(IeSx|Cj>t`XgLq#2)CB)cB3e@dt{?HdqAmWC3rp0-Tvd}dzr zc<^M$q}}@B#34*xaUw|$5%rdr0piF$hK885VD|=s`oe-V>brp z)fH5xgkt2`-TB80udt?oHu^dBTL>z&;+YnqoF^v_+P)of#+Hz4u*9!1r*xB*C6>FG zR~Y-VdjMG^q^#<@J@_7GQDW^BfICdj!jbUjJy-1$&Rk?N>~C33u^Hh3S5oZNggX^X z8qnKJt1k1Ux*_fqVTlloA9}|yEU-kj33vqj?=UW2vMszZ1OFu_bSNb^-hc$Z;T{Xs z?-?f2H81kTC}4Bs56Coyn2|%JD$^6p&V<14ZxECFa00#&4(_leYKwFuGe9SQ;f>U? zcXH?lh{h@lb?sqFo-XQdQk49aCAQr%mMo+B56kr>+2%`6B_L+j%a5mw&KT4Q8-H&` zs^_vZuW5>dq#Wm#1SPDnlPRZ^bop0^TGUL1l)9uD^2!t1q=ru@W|K!MX68>qsGROl z>QWPv6UIZsbDC=d^j!p1a)mr9$wwjKZ>?IKv*k*?DUC2F4uA0J>wok|2w6LP=-k`r zAq;Di;_gH9ql@RJmwu7cmal$m#a ze*@eF2V_0*T0@gj7~wL$8|OR^3>}v1&EwNP?idktIy8v|hy^lYz6?#HXzFIkQ$P-6 zH#{S3_-+%=>|;l`xt%C%Is~&<^`@%sDg>cKTHB`GsCZ)WdZ#R5G<1PPgRkm`nfdT% zKs4~Q2uDj%3V&8_K^b5@>T_`0PQaJ1oX;4mT!hJgkV|dU8qZ;(usnUq;R1U3{ZD9L z`S}TKaje^)P|Her^$LASXF*}~`Me$$MQkGRU|eJH+g1PX^y-tCqU`OaLI5U{_w1Iu z`i1ZBbgFRMFhw37+hddpd3T5l{(I_iDzA(Oow{JEhC5%ne5^->y9tLgI-tuC!N5Ch zJD8Di*q@!=d27}37lp%LCUZm%|KFr>sZ+VenKCeKxv?pA<%ii(jK^;zPh6qeAU>hx zGzzOlpy7uMn;t^0F%5Mw`n}Vr8yIue_&x&jbD+c_(QzOacuF`!97N0+Va4db=3n9D z)~nAp&q8d@0ydNz=KTBRm)j44dLV?GCal}(G&x<6m>#n_V6y(S=|n&)mEXh&BN8vv z{9^iu`1QNNq!NeOolk0AmfpKbyvOJe$kn)(Hjma+dfPG69IF)o6IOy~vV*lkY2l0M zPNRlRlyhNp#zNLtCG<{!ne8dLT)&JYxycq&-*b-*y^W~;XN7MKno?-6-m}8G1Fwx> zPT;GIQ@ z?gJDRHnugBVsvP)O$g>1yHiX7*d0zt2<-{PN)pPKU zo1|*vvdpnT9atH^Z9gU*oQz{f{ z={LI6h4P1fwUSv9ifJ*=vp0=fR4i~1%`s%HMB`z_6khKHJ(nklCEC(pEpQpj$1Jmf zj+0SDc8W{ce7uEDu*cANW}evs7fvV|G?W}sRj& zl`%b-Vz*=ksR}P4`+ODt;%YI8mu^ID%cuWJ2qZhzqVl)Ark3j_G5jj&DR2-u^->kU zi2xqV@mQwtYYhmzj?`f-r&JB^Num#3+Qufl*an5mu-7Dx@UR^XsKO>Qn&I7#9)c!D zUs|@igbxT_Mz=)IgGIauPq8@)S#zeGx3(ay zNw?cnRRy3is1i}Ha8a2&s7VY(zfmu*Spi^sdHc&GQPf+02&tHqre>E8vMMqi5wwLN zdDlQ^IUi|P_Tf0M;+Bm5{LpZ77Y!b z^fF{c?&;s+uj*937QR-$)`#hRy zN)1X#eEG|Fc7eoS?&iuYWtbV711`ALdYlE^x46{aDVY*8q+2MHt1*+CWLy#C1esSt z{TjbdPj?Dw+HB%TrqBc#e@CB*vYqNwQ}>BlMX8f-8TtE8ImiR zlqS%HF;v!2K@c0R0yNjeonRHV3{JBGa5X!$)FWCZ`_K&gvab1n-=2U{i|cw~pb-&= zh4XUynTV2KzLyJt4I#i#ZK-&~q5{@}6L$3PoIO0S#{-B+CbOYZY`n9?hLUZ;3{!tC zebUU9!{*6qCX{xiPWCtUgB*X&k`S_@>ei$Ihsd6ZF_8Pd>ghnwxo^95J`FgM{Z+JB zQEx1z4S|az+K5fDy&Fc$GO8R*z6?b(JHOoIokTZ8r=-ETL`J3Q>)PR6hsU$3(*YU^ z?&kH96~>!`tEK|9kh_#3pC};=SB^~qQM74IwiKxuN-WelI`2tIcpoz1=7^JE3DD)s z^5BnCUucI-Hy{!X#=5QId3h?Wk*kUUEmrbEbWQYQj3H$CFOoP&lD63>u($nosY{)6 zo&b3D>u9S0WbaeleE$+7F4(M;cScSL${`>iXVyjA@DXP?Rk~+Wpjglo^EaQB#U2VK zQ(Bn3CNNwWhpFjP2A56Qxg7$nR3ZWq8V=6s=S9o^qYz7BQlKG;vDoKgQc3(35UQ!D7zVfKz<*Oaoami!z&Z&wWWUyH;%=_e@c@WxfNG)*JC7t2M?rsN5{-e4) z;LrQz2fuyYq`V8w`0I;Ke3;1UUWF<1YBgN_QIE-Yu@s=N%P9m)n) zrZ$>dRMpT^EHhJ5SfsWQTX7F4lp?Hjg>V~vsLjZn*rNHKP-=R zC%XxRH#2C5b)w)jbl_~W*?}db3b1HBxq2`b_$37tRANtB0VGVDaF-J|E)MH4vuFwx_L^)q=haro8m#;w12(E4_A(*<*c zNIKF;SiXwMz%V5xG8zg9wFm92@E=Y%M8ypIuD?c1C;v4(Kgdq+48|6I;qa7#t7Qc| z>6PtYq;=p+5INo#R{R$i#i+6O{?~S;KwGrE4x`-$;V`rzG%O6OMA5g140J$(_}Yv1 z=+w|5^_?>;KE6mxs>(-7ih(FNJkE51)L;Ptxpfr23f{g|o~$+t&lFm!@R0B2T9*oB zn@uioxGT!`Z}CeVNTgGOAK>vGO6)y<>k~4BD1BHo6#)ne3drbeIaM1vZz!eGeYopx z5729_9Tzrrm4M!=e#{urAWgPk+!aCsOiT9jIgVPmOSY;2OD0$`5ND4w9Bn0lDnW3xw_l$KO*e-QGfyMf$7zBNO`Y5Y4CN7xZ6A#Y5Ymx6a(i#1^pp{^abZF?z-h)Pa?*1fb zuYqGsi>jlYFeS&is*kCY0I(k_%o{w44+tZp-zWyK!94=kr8MMN?&3O{=wjGM1JqYcFDj2!@Pe}wI+K4X;s2?H0z zX#0Zgt&mGQ4QOkdOa{DvO=-ESFW+26%0c9kHzsP#Dfv^=1kkMRr|IG70{N`K?og{g zrAHCkCh&jfScHX#?$-4)AhHBxL(kO_@g8^JXN$ul6SEZCgLKXy)nIgIk=oDef9Q;| zwUEV_^S6yQ_I>XARh;g)qjJ)|0^Fzo@6^1X)h1S(TywYyPwkax05(A_|2jAaz0mGs z0}_R<2OzGORh)`ms=%s`)r-M(s<0!Nz4sx%%e6eXzaT42taekAgf_|mNO*B(SU*7$ zqt%6DF))X%r<0h`TGGm`UqD?v`v;5HiQgYDFzpVHe9Jq@t3DTC3+gfknFl*i=eKh@ z$(_?=@Ecj6`$55LJ%SId`G_e^v&+x{%W^Lj9StBZoahkv-q|0Q_x593o$lkA*Z%o$ zoyg8Bsl7O2e}D&qL=(+6_2d1r&e zjkn3RR0)AHc7zMS@MLexskd~na5jCjs8J2bTZOBDpZmv|)%8LD`XnHLmPGJ!nu>Gq z;q0%32UE5BF5AArq|Ox?wOitUINzpc;IOhyiiyF58hJ^ID9((T0V$HodnqlD_slzL zdsR`$8msFS<@0vvUOa~I;kNI7_X2ZW8N1_fKn<1NEyh!ffcpXBxkZr=UKTo}CmLV_ zALxW6k{=ElL4a*d3Y(>|D=i?X0XPP*e_0s#D%kC`nvog{*~Lz3|)qO3PI3(b|Dni-QH06iewC=`Wm5&nGhxZ;G| z{goZN#HiWtKX`G(qNaOnQhtK#$a*Jom*4L?&ma~w*g$*4+ye!0YPkjJo(>B?BiHi+eSi*X~p(b*c&6~Ydq{D7{QR|he(nCCu$&%24*%%OJ*gZJc4HY z;b586_tL0kPR}!oXZIz^KFM9+BQl*`xnrANIS%nON@0*?NeL4GW==jNFmc}dH^>#cv zKE+nr+_*4_UGzra+^Z$9Wb>B}TbFIA9AxLu#y6TWAp8UZ6Lt(ju<#?4NNTo?822nv zmN7gw!Jr1v7i6{}WyGcPhX4%A_8(@Sr)Bqpk?U5~1A2tsg?G6z*Ccz8#+zef;)gVc7^kM@AvK4V zfhPfsD~L2We*U&!EGDy-=?gKHthkW~OscMQb7xy|36@fos?dm?9&l?8bFVO~&judZ zUEkxqW7QjfB`!A}>^Z3V?KiuIC1CRVQNP2xiHTl9{hRr5`x!?_jSqC-&V3ow>0DHgKCw4jaWUir(R{C05hGe zUI8j`M?_sIMT`A+0)L`lbjyc(32DhS1Lxe;?6jW`j?{#2q)xtw^`b%w?}}?-E;Za>{Oo3@o%#@-dptcudQ#@gDU8Y9uK+Y zuMDuLuOD^Cd+Hv1{sTyYMit0PPI@4J`G^JRt{Kov;X>C5z_RTF8ljTYt<$>1N z`4APGspv~yreM`PG6Y-r5-8DE`8F!ss>9eX`Sqnr{b4E9hNU{Wd$^MJ)x!Fi*{qiV zRv`+c=!TQ&FD-^@0U}jbgx0AF4JfMSyJ(8>GVtiMLIcD;j4U$zy7o3O13H?`0~=~w z93$B+8j@4>=brlbr)&%!VX$p?G~eM*y4*uh>4ct^rt(!9m=$;-H>!33f$ZeQ45oyC z4XNvO3esu(^atqbeyABi@;>6Vf_;US^jPDBW4|;*t|JD50&aVoaURd=%YiBaJc6ub zt_iRo@B*_y>!9-(ATJma#Xh3l`z`get7FMPkHw-el*PJ($_w06-0kH0sU<99d7RrD zB*NF@s9siED$YO|MGT?xUTHZf?UA;f)}6#DOD7s{Z#%Gl@bQStq79^Omse=zeFtS< zIP*$3IB-tl3Y`ungSraC2E^s@ZX*f8?-qW)tBa{i02BRf{rV!avg!$*k~`;#t_KHa8vxPF)*@ z@Kbb2ajnYl5oZ~Mk@i+W(QEI)W}cgDoT}&G6(0YHNUBnjs5@Q5#{!3>Yo2#v$hj@` zs8qM#W&29V70i;_^*UmHgx-rx%k{CqR=(RW=NJcnZAYp5893|`gHSZ2_S61Uu>a+K zIoa`Nzx~BR35Ls>b}j^TFjK373Q*pIHR)%)+EaU#0uo|bG_uNa^yVX=3)JRKMd$Y+ykTgFDZ+&b{ z{}g2FohU22W%4E3z9O_oxC6XpStx+f&Y~M;g>SdL&hqQP$LPdZJ<-a|$#-tzvv@jl z3Qj_`2vqqmu1lFhozFSdGGD*}+Ft?JGs->!kGDU)3I5+VD(S95DFKiOXTe_&<13_= zqb5b^JYrr;hMNT~-+XZaftI^D{TETwwehd68TXZ_SFnu?W0*r*t?QX@qXxr~xE3?O zaD|PMZ+{oY>$2!5)QUil%xviz?flGzCJRkRU^N@4_7h|K4-s&g)^nA$iiL>w1BCH{i3$VYMDQAM)TI!F&psr7~~VNV)k_hv?}IAGxs3 zQ2=%J^revNn0zkQD_ZOy=Qoc_}p6Teb#k|bwWUg6wZa8{Yasxa0N!a zl=U^4C1$%kxo{X8^ zBd3r!yIOK)qk3W~=paQptCX~+se&<}*~VdgU8KT9FQUtLHvYE>TbElaalDmZh`vfs z?ONHErh^>Q8_(9OS|l&&L=Txc;#pHcZ(zw43cHDGY&sV$7MfeQ(zgJUEe2VMmr!JN zT29t1dh-|*@TiCU8~JiQzzIrP*pXJZMZ}dCaIEQBMH4t})m^Z3c{*YUQ!5Ly7h$0Y z`t%OPns%0Vfu$trFngM>U`_lCcBU93s-M@~zt{-lyXrpuuY7J3`j4a`jf;;fV&QYc zGzD!6$p-WRXmUh-apiMmEQsP>6)*a#&x=;&Y~Agg+45~oND05%0c75RkpnYR=#YSTxi_HFdVaTVP#=ggoIB^43w~6#DhH81)BcpB+y;hD>#l2qB_tS1_ z*kVbxJ!>%pv$u5cvG;5_37xJ1CW|FHr-}P$69pnHz6aKfv1c~&va-Lpog=zfG_C~B zLoc2_XeM>9Jw!rl1PV{@yku?cR8c1&EVqVdHn!)yLDg#gP88edA^tIW(6M-;o7$H) z3J0ED!@7t|8a#)KIK%w``zyQ813l5=szO-nl1k@x%6iAjDb^lZz_FCCX1UqR`-7O- z@$uv#WYq>IxKyLA{QKg0m^tFk8HBKxdE5*^P}F*y+5!NxPnRxJ1DXVT zsqHbDTG%>F8f}#NRlRomDWzd0YBg7HUr@|(bEj)QpP>>#8vml5*V}U%1ig}cv(YAX z`Rn}9$!utt*sI%F%!$i#%9k4>n8If4MRpOOq70GS=C&ou1ysZRmss8X0gUN*MYnpk zI@yq~zYPz1pX)yf)#V<+CxDFjR?A2!Xu;{CKRc6K^g05(l&)|)OWWa6eNjw9YzivNPSVArc3y@E!mJFRm1lMiS{Zst1H+^F9>pQJoK(XjW z0UN3Fppx9$r;{oSSimVtM*yVWXx>sg%zixK3yZwv&}}ohQy8XB8EbM@iW@%M20Yr^ zsIMh{OOqy4d;vj%Tc5FSF9{&AfRx+)9V@Y|D}HKxbqX-b?)-^##CcDOh zKUU)jpc6Fj^Rc>?!aD3j6`)RJ|{L|J2OocsCjHmSPpq5G(+72 zgrVHL*fdw?-+1{?%2wB)^sQX!%dc_{+3I%&APdr~o8s8bhxw3wYq_NoMD=sbn6j;t zUuVo-bR`_UN?fLw`IYW}@X89E#snNfS+S36qI8+V=j_REZ7_P^bQ3rSHM}g>ACw|D zQIQLU!Gn8+XbsOyueeTE%Z4`$3pQ!EpV#@!D9*|_p-*A|zWoDc>u-WnKO=-qlas|G z;6;d))U~8?guq?sIxR%SM?;<@9RkI&X@5f1LpQhewAlB(A+6SIRRvJCA0X>k7qw*VU#~#sgMQw$+!7*Zqf9)UBjM+M63`P`^Ysv{}pE(aL` z1A`J!kUjL_QEhp8VH}cbqOArz3T`v_@$$;H5J~w_q=PQD^LuN zhDqD(?YkjmQ%Ep*x>MNzr95ZJT%tl1b#%Y3mJ_9h^M^`zSF$qBI|O|rwHEX-&1FQ9 z?BJut1pg0<5eQ(Td-oC&e~X9iA)wZ{msRSNWB^gEdQZ=Dp%8cL^^(air>7%#^=obk zw_LwN{rR<(hiFGIq;{9w#Ar1bNB4Md%_ZYA_9KQ8@UMguli*U0$W3i2-`Vl&CnN$L zF?@WnNS%)}SX)GU0|R!HrAl}IT(ESUnwm|&NYoskgbO}zxn~$^zy+d`e{xjMgQOgO zi@*OjC$UR}-`b#VCBd6I_AA%x{PS)f!S4@U4-eIH&|&_-(ozct-5+_YJu@!BRw8%t zPad$Kq)XpGwv`h%4xvB`(qfLwYsElg4;O}0evy2G_|?)Sw4eVNSA$3rtiDtpAkS|7UX z7?*HY*7KnR`&py*mtSLe{e78r2{_&F!r8J$7%;(8m_%LPrSU>O?pL#)oN{PQcw`li z)5OAm6}n|Ia}+b0Om7XVMp^6zHNIr3qpJ9M1i3Lb4E!(g^XYmW%JG^Cd_g2$+$-bfYox87<$w*~4KB8)+ zH3s``Os>NtXV;ngcQ=a zL~@J27jiT{8$Dsh2%XTB&(67@h8}#8H`N-Ju`zfG`G&o8dO?V3`j|DAUB${n6ng_ua?hG^Mb;&uLI1AdGAl7O^vUe?MtnE-feTDlr-8Jm zydq-|t%Nd;-uK-1L^E*?tjPRAGT~eSj1`(H+5HQLWpE*U7QJ$R5VgDH)A;OR*JR{f zCoU!Cjnc9Yn*ITd!*fnzb9;q^4<51DAixr>-ZW$x(I}53j%jRh5+*xnjj+vkG^Y_< zRL+Or$@rt1sa{4@tj<}y4}Rg#+MCaQ|D+8G-JvVc3)#kQmk#Ib+6kj=cg6+v-Xapg zP+0a3#Z$_h>r#~P{jNt-HRqXk|5|8uoD#v0$67Eq&)&s*2daQI!lO);AE${8yBmot z2$@j~5*k0A*WpE^Rzg9(jn7O7UP}^harpK9iu4eDJEpV*{=$!p>#$E;8|`zOQI^t2 zZ6pIEHaz(*t3q(9L4wvFO<(K-t2IV%w9Ck9+1nB7mMU62gv023Qo*xC>-6&*=(G~A4uvW|I%a)}SO+Msva%6xOaIT6P^0<1{rsvFF zPvHy4hn#0uW5B0IU^Rt)#|oQWqv?(Xvd;|k;e>GQ_&1X){w*h)tGNtGkEoCNRbDdC z5&Sy3ZS;?0{=Bd!3KWj+84l5EKY^H3pONidr71rWu}C$9zW;NS7q`_^LHOvc*P@ok zgQNF`c0(zHI&f1XKA9-hm$9&*Q;4LBo5MLu8VGQqRl&QRuv>(Y6jHmh<$cx6<2~Ce zKVpGTsF_Xnkn1V|8Sy%X%4$oZ8L;UR?)#3vseylXQ+N;zvZTd4Z z;#y3?dw%KP(6Jl;y0YcfNny`#QGOglX>m+=EXgYZ!&)r=;MMZWbmFD*L0q01*h_Bk zI7}=S>*d`91OX10hlg_#d~HE-1p`^~A&bN?3|fmtSf69A(12VA8Z7 zVKI_?M0%&wG$d7W_6sjJL~I&Wve#qy>D+OAGB+xR)!S82vGv#5uedV#x~Y+468`mQ z6aw1_cCn2!B>b4j1zKfl;9R7%I6d>b=bU{rAbmI3ksIfWaxl6+hnmTWzkXg z#0Pw1pggkylU6>?H?4E{kBs>C)1+0fwbY-!TRO&S`8%b;eEFa7Yr5Yo!)5TsubU&O zaW8+vYm*EHm2-&uVna#7UR-HFU~;i}2%QeZm?>*n^&JC?y&e=uM$D+x8@H3(p!YyXBYE_wtNQPb+puN9=UG6f=Jv>nZaF9xzf1`3Z~`ECR;{SmrQ zw&AYB-Tzp*A(g$!OCqS3y>IE6x% zpC4na^RP0b7oH_^m5=UY)cPvB>U!^T#54IHYXxP~<>BfOc&1Q|o#|nW zB@gkM%R3%pvj7r}$JpwWa*%N%LA~)ncf1a?8qyPeK6^&f8xC!Gq<$pdDF*4O+BYKu(Qwy`23FF;zDzcoQ?(?7RNBAurd3i?d=k2BCMlrOalRs*Ef^+z-PSjwS=x@uA{;A)Nk(V`xKvv0 zf&-ZwoCjDIR`Nr_EFn-3t_izgDEPLO_}A|p$ldK+_cT-zXn=_^Q_R6s83(?=gkGis zh7fQE-5zdY6(Q}U>*m#H`xU9dAZ+a7n=Goeo57bldzC^A29HQ^*lgKqr0xUK%&RvP zK!JHehnmheeRU_@gh(>kV8Z+AIVHGLH!hv(-p6x+WN$^L9>WLd+TP`HH1dl+F+GKY|XlncB%Og}?rlwiU3os6k z7r&u5_H2cW*u|VSYTegvBd>|^-qmlTxQempYurue{9jMt*QnV7JHizG(^>vWYk8LdF> z{P#!y;buiy@BW|kc1gO(2F{CCij@b(3soUyqyv>OOb;dk=P^kSrGt<%W(iaJzGuea zZjiDKFrMo~-DE;mq-vv7h7y%2(Sm6pARs4!fOUdCFkjn0yrc`xO;^NFh$-^f4i$rv m){x8pDC%oabOj%L1C7J1xBg0I`}{dW36K(#7p)OC4Elfn8-4Ep literal 20394 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 diff --git a/app/src/green/res/drawable/ic_launcher_background.xml b/app/src/green/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 7fef14448..000000000 --- a/app/src/green/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/green/res/mipmap-hdpi/ic_launcher.png b/app/src/green/res/mipmap-hdpi/ic_launcher.png index eddd426d3ede695ba8157568cc89a977eb947878..08960b9231eaa8df57e1bbb20857e7fc685e7bb7 100644 GIT binary patch literal 4717 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 literal 5689 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 diff --git a/app/src/green/res/mipmap-mdpi/ic_launcher.png b/app/src/green/res/mipmap-mdpi/ic_launcher.png index d56d05c13ef27cd3ff4b404c48a8fbab6a7d35c4..1a769a71c4de5b91dff1cb763099f31807f42800 100644 GIT binary patch delta 2935 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 delta 3383 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 literal 8407 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 diff --git a/app/src/green/res/mipmap-xxhdpi/ic_launcher.png b/app/src/green/res/mipmap-xxhdpi/ic_launcher.png index 73a09c230a0b8af9431bdc3e6b37ec306be8af9a..5de70d9fd0a674da6969b8ac00cb1c822916d7d7 100644 GIT binary patch literal 10388 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 SysYs60000wWa@pRVfK)zw{9 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 literal 20911 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 z#19A341 + #097b44 + #39ff9e \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 349686902..5600bbe2d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -170,7 +170,7 @@ 4f? zCN2TMc3xFc{*{Nx?mR)#eZ4c$CAaokKhtLT3U5H+}6g>!vr6KA{Kbl48c5}t~IQQlTc}qn_n?4 zNEu(qDXT0WIc?K=*_a@?+4nbZCPOv<5*Ih2mCOtes}Z+WmKHz047!bO4r|GqEmj?E zypY^Z$2@Wi_52}mi?QH0_ivr>qWTG_wb6h_0C3v2Or+lN| z{NM9Fxa}2CX7F2y1y|}R)KiuT=?$F8f%<3`Bj|WD%q>5!)2!~Bney#M0fA!5nmXnx zI!wa*8U6Sf^grbd6iP|b6wPZX#&ecF8Myi;@l{#{!85No$Zn<6-mQ>ujB9iURPqCw zQv&?3+7+ttqiWGLq6XWxx3Eaf$&x+l3 z`?pXN){6IyqyETOK)_j`rq|wuZ86%m5_~`24swvYtf61eNgrEm>z>7NG_3*y{d;95 zzd~uFqjaO+J=BzOC`Ol4R7b)WYgEn(Cfd$3zk94{w|BS4??A+uu=7ch+#=A8dlH)c zgS5PKpW>Qg{_Q~|#rR-{KRN0JQEDUvh6KWB4Vm`MZZD(RC?49mdNz@ zE^G!X_F8R6SjO#zLAKn{$#Bt^8->(G`lB|ifXR#fF{Z*a1Rru**;-5}Vy1i{$0)EW zMDgLZPwZ_WhKy&Y1le!M`LKZSLStYjI&6I5EW9vq@zHkrVb*O2XLwcbjBKIRKk5my z=ONCF&BC;dstn#_MTn*MKHN9zqO7e7yfqn2-+=$lBEaepI_vR@$5^=aTZ6UZRTG{j zsYBF~+o}E#r`}M3sKfQF_$4b6T?U<2{NHjzOr9< zZM&*33%kOakY@YU^kV#FIz_6He*t7|XIy>UiF1PWGS^vCfB7=ZfzUA|nHcr9tpRif zFx`;an_h!#B2!ZBvXNRewUhLb+jbW61gC-ZN)VY^=B&QnZ65d#CDAtX-{S`oehb<9 zq?%=a+yKzvN{k8^-wusX!026AfadVld#ATgE0hGD|Mx5u2bvH{0hHlx(xBs=)4Z09 zfwcOPN)c}oV+)hED3~@g_GI_x0=|pp#s=#c z-Z$qW-=Ya%A7+tp?JRtw6Uj%wrT-+GJcgKIumUfpk^IwvWW*oYLQ1T4(UK}7cxJQl zxb>H9fQ4$6mZ65F(s0SIQl+W=KRTXvC+QdVCgCA3fg!8|92y@ErX`8aF016{mhY$Z zh(d37(oOD^1m>?Ax8faqFVWikiPFnn&Gz;Itgw1puwv_K!oDcDZ^sH1r$a!X?Tjee zoorP6$A%{;*}nSJJonyn(CW#BX(+Od=-?>qU5qI2D?b2H5N6Et4s;ahq|tTZIX#{?Wo`d&suqu)X)g? zm7r}EG;P_CAw9``7 zj1GH+FOA6H3hQMra6IN)7}JhC7Ovm!`}7^AWf`J(CO*E*ch&%E7gztOX#qNe=D}x?H}l%jc;0d>P!p z0%&xxjgs8Gc^&1*0v<0&Hr2ZafeU@|pBdN5$|WYnP1GK>s>KM>5{w45d;Dv1a08ke z&1ySD6ISU$gE1)7J_QvU+eV7r#e}JP-B62_6!Sj$mRo=!RvzQaHM?odrkO2BS#JMf zj@tKr%AjZAM3{3kN?X_3COR82&%O0fQx_3=8Q9j==O(6p4S}p|=pl6OK^eeXH&YvA z|1hG_dy_Sr`s#iU17!HsnB?o*(+7tFAZS!L8#Oz{zVFUt?vwZsEM=dJ-V z$*WQP)R=c^T|#VPgz0GDc z8}!}QL#I*PjvQIAOKf)o2HEm;5q0_r7Tcg_sdYVFIn`vi+~EC+Ds2timv{0-B@b=u zUNXDgky6%=h|r6ItpDdzA`u(zQDV#SUNE}qV=-DIRB37f&zi+H@^{~2$4ILnE)PHd zM;YPZhOU;6<3o|nL|UOX{+EFjJfjR)zc#*V>t5z1+Mr6luR3yYsEtJbUh;HWh}4qF zH)$yFgXV89sw&(_6)bxBPX6$r{rNE?pq>+DZ~G*&fJ1=N>=Ra|yw&OYs`G*}zqo0{ zvqwju$hqIv(yiaT)_scTwaA)SehvPywbc&RBN7>>h~QT48mW;X=s>r4^CpfJpsAm{ z935%PU=}08f(fw^*~pNcDAqy0(+KcTd8UGkNO0-f{JyJ|U@=9>E`=bFRn?COc|BAl zYBGax_5R6mQry2y%qUz3k_{c%hZ(p7xL7w78Pxu`#_S?~bDp1oLHa+3bKJX8qcrZ+ zo9*tu(A&#vkfA*ux)nrr?Xss(Y|uHOK)m4PO;AP$?R=?J!GPTiHz*Pv&|RJ^^2-r> zF({-v*F7M=X$HC4PDAOFbGD_kKS5NJHlg^@Ue$&*?OBg*W)Auy&laxeOy2F>fjAtNri;?=QH1&i0TE#+BEiN0Ii4(^= zOI^9O9q|F)CISRl1o=M0x5yT>i_Em(Pe0#abufBg&^^mFJn4QQF4tTmYGzWiWPOhr zbEVN$K=WE-n3;;#>OO6Qv%d{dEuZ(jk3>!%_H;`s{~d*@*~PLChd84w2@olfMsOsJEdGuPc; zkf~E2=De^Bq#1*C0*+)V=mfI=2Si@#sJqtr9I2Y zT8|SP9yr_)o7gAcLpLn~{B&!3f%PNNPQ=CwFI1u>@E-8nudTYY`le%I{FIef*TO~B zzLgUj5o70T_PX{~+Iefzz0kIiN#p#F2KwUu*`WyzJrp(E<2RgFtq3bVK+SZV+T6^C ztbHj^l~73+Xo@__JBaev=wjtW5R?UiHzb8i@M>@GhiJv}_j)5FU%(yThR`oSYb`4&Kw~)zg!l1tcJOIWcN8!dV4llfBId`rY+T3n~KL2$Y zgAvvkU@3M2Eb(O~4PP6qe0Pt`Th+H>j~@tcbg2t_lR^1F9RnOnKp(wL1C4j!75k8k z!h~-bA4wbUG;5(4m%Kq62j=b*_$_#c1pcJA9t?ALS~G`x;o`>c;|_fV&k84*q+4RY zM4fZ}yb5|gG$nY$Z7aj7j?!E*`fn_`W7RA#MtEHBayCGpqxyWI*qi--*DXA4Qfpw{ zxyM@95a==vWb4FT&cnsAXR6Dx`|gmR?rF-|HfcPUd?PH($WZRh4`uvszneet?TS>+ ztklfhsGDrFP`P)IpZ$-qhz3>A=q}K%{URC<-{$kcgXur%StL&V?~3VG81TxCs}@VY z1L>XDL>}(&TY-V``}jjxfx$I|Gg4WVA#+)f5mW~kZ1N5MlI{N--O*?6QzvQVP6qw^ z!Jk@xwOkc0T)@aI^~eWa|KC1)x5>yqf&12zSgfHq2Sp=@Smz!8%W<3pNU) zgh&nW5m-w*RX!SCJ|DPAcIZWifcg;*s%s``OYr74D_v3Ngwfqy&sWb{lm)QSqZ#Mv z6-vAKUE74GxsY21BR{6<^uX+=etU)+d#NR*o#PS{mwY3i*z_Xke5>hY1mkbG-0pyp zrAfn&x0=H@g40)A2%LWqQ0Ly?DVy*moV-xoB@CGrCsYkod zDdl7>&!k+)Z|h>JD0=MM+FC#Yx&7k`-);CJ#6it7SmKtfs=fY4>NZSR>=Tcwa{;Wc zd74Z{nEOsfDZUw_+Ig|$Z2PpQS+u}LMm(-TF=HYCrSuI}Am(V=Zf;d zRXd`hzcBz(3`oZ#1jN1szoI)i&kMrE1c#Tb$r3EtHl6Hu6**Zs-r2s8|JYaXs&G`(y|uKKGzqOgmP}@hh{*6<;RllOi~5Fh z;E;v?{|%6Oi)@KA$G*;U$az7>nEPO|yO-OZ zu7);jSr=Mb927h<4G%BBZl%HaY;57CE5}3|(~({NQi=<)TQYoI-^*JI%ZY#K zsr;r*rs1=E!_hgANmj^5=TO&hncxJzxd$ zM(W9xL%C$~S9}X*YXqyE2MTq<=}JFd8K*)edW9MZLut zn;!gJ9e8|raKkXOA1xhfr*YvfE?^}(FD`pJcOZ?xhZ-)o6%$*lu2>Qw7PeQ_$aH>D zo!E;rQ4bA;mAo!{0RIsKz?Du7f2ivy5yTTD@z!lzLKhz+lb)J9>*;(=ydt7)jWG%1 z6<0i*p?lSICKoxm(DJzEm|tt***R}tg&hY~$zI>Pwv|XDOtY(^<>|eZ^QR9#Q+wJt zUNpr$T%?v7b6e*%y_TeI?>_FGE;15?2|j|r-n#v`uh3r&x9e~6(->+IH!cZF=C0mQ zET`suiz?+P4q6rP?K#rs(c-SAV<@91kn1Y-L+u%1jDHei($**Wjw)K3FU~#X=aiAj zkW`SfOcX8yvb6WKe>N}Ezh*Q!TY5K|E=Uo-S|z!W7pt|dsn+~_DUsD0(N5Q3B>Y%W zoDq)ldZCp$Jr>xvAM@a#rwv#AX@cD?C>YOc=Paz(IxeZ|o#X$e1{tPoBti-ZW!QUKaU zvNA7AQ0_u<{pMk0VF5e~#-;7k&kxrf9Y^DNT22H=4*K-V7+_A_YJ5|1evU?w`<#5P z_R$hl0uUA;lcrk2^yDIu$s}q;lrjCKbsj4ud_|P&X6`QfVcJ~-jnd#8D&UomC3|Si z#nFCb@nQV*Nl4AK81V}bh>c)~Sr@loymmkAJMD6=%KLI2E#PNg8ele}R&>Jtjjtr^ z15;hC?PUD@Q}%f3T1~DM`>FRq5g7e9I3?v`veG3vA`p}PMZ>%(13kPQP(bm|I6ljp zphCpt(bIg=J-^65s>1B0(@*XV;D8It31K)tl^kl>UuX=3EWdG}5E|juLXj%3iVhU3 z1cixIHBFQZ@4m0p88apxQ>uFW&+MY-h@Q5BOUpHkZe3R~2d%D1hAT2wo)lEZnJsUE zTG<$MEGFaW&HhZ4A0G6)*2XcU*^)I>dbhs78wA@4Gg!GZgSOykgM>ri(bbBr6lIsX z9dZXBO34C1HLrbTvN_2_*J7DWEIW>i=wBfBb@ZEl5WCZVlx0h8(2UAkcq`CirPFYG zihh6{FLAu2(6*BIY3rzyis%Asth@Xg4!_AVc`2*T9IijpWi~t7Qh#M^Mmwl9IHP({;{+cFgtrRK|mnkJ8LgRZvSn# zTb6EdAgymdy!db*zxMa{a%#99XtCm@!T5G<1C>7G#{RgR{QB{&!Ob4ezc+R{Wo`id z^1kNJ@du+!*d=1t`fFjRmHNt^&J?Lyy7dM8qxylUI9!Z-EsKqy22p9Id@=Fnzpq=atMOvCIjnfE zizGI5c*=-JtEOL(rHVQ46QR5yMxX(wwo4v}HylQD9~@kc2!HK6k8wI^^mQBVM4!5hC%H!&ML;6%qqN9Rkmat>8}ZDqqH?{(gbKFNJKfH+QV^rq)t zqQxuIS?aJV{0Fk$w4vNGYJ6f;zyO?fG#WZTD)ep!%0MF@TF&#bq?2f-;Y(_? zlTWVu#5ZRrE%oO9-h9!upV5b5d{?748-1Ri1y}SQq)PEdEGkBqbLP&j=WDa`8rX`k zz_w3a_KR>Z3`@zkQId^sKW;suHE*}3#DXK>AKv9*C4*}FU|Br=d%ShMKxaesNj4C* zSn)dFfl~5TpC6TNcg|ztRnak^T&ls%GW*Rn+)xZrynNoVJ1~O_)BkcX5%<44#q)*U zdcXgg(a5q(VMg%rPaCDa*R{zF0)i${J*Bqw8S=HgmDxTnxT0JS*8)`j)jag$*I#AL z*n|LnvZ1WsDG|XItjC4ew4Hk&E}HmsHlFYeH3dM{=4eXafeaqXJ232x)HUy<{a{kebK?Sh z{zAP=J+4}|%hhwrf31(ynUXlYG~yNVV=|Uxdsc4==*!_OK`tV*5d(^9&J_=$+%HL= z*uyL(G6@(-lIW^TT<^Q;QNRr)Q=HVoRsIHfeY<$~v-g_>i`#L9U)r zaH;5bx9xN%1stTS9)L;oUeVP|;MG0+fJ(aRT9XY2=>heB=4zx|h*evo6u3sML{#*v z|DiH<;{Uw6)GOA4@4$XQ0~%P~2^x#@yg@OD5S8CbqBY4g3V#62d7KfEmL2;I>#qaw z;Dd*_9VH5Bz{V;I46#wmK$(wbr1KZ$zeF_MS2ML68hv6yRVU;WXFbVlDv7P?L0w9N z@WrdqWN^x{YO~$6-^L~6F@A}W@a882Z|-%-Bsy`CT0R`iS($G5eH? zSE4qrtWQFw^QmCsUaP^PxU2@A0g86uOMyyLb?)YR(P93yE<_5fcZqfz1N%vdKc!v( za}ah&_^PO`ohHs5uVduu(ew6!9$O^ZGOqp~AyMDFLUd_QXFNbdN(dvU`{0GsnG9_f zgz?s~nQ5MQ**chI048^#$JHKTJQA6yixD2h$Om^P!Pv06?i1mp%uNZS;eOZHAPMrguOwam!A8^La1EYb>Sfuy_{WT68ybUS%Bp4JvgVrAb5GZ>k zYw%`E=Z_$J_JQ#aO*|vqZjH_kf~yQv1;P5Eckkr*ieSr7QgF&Z^yWh619ObFuLn>U zmzgT$q#r$6}{7QJ>{^#F>IFA$3J)}iE8>6q)!wv#zErSIop7~DkIRu@fuXiE?mKl*K0@3t2%e;RBp5} zxaf9@EFZb!gVmA)0ej^ybxQr9#1CHq{3JrZWHFhOsK>CrjZdOKCbeQrr}&<*LKDt- zVp`d;jtQC5Bcnl6d-Wp!%O#t(cvCVk3E&(@LDS^n?g!`uMm)EkGQQqj^8JOgG)R# zp*UIk#uM$C0x>%_7aalPxjQ~N&Fbn;v%^WXzX!8GLXj$pelcKl8RA8Nx(cWP4*ct)F7qsf)&k?msANqRQx~Ls_f81ziI zuaTYuP&UBuM?-=`Dy^0EL%Tx{a$dFR=QeiI1yOAEM;wEp2K>r6Yx~4gU%^3#xWDE% zp!2wsiS5A7$cZs?3IIF zz8Zxf<$iwju?woua8g|I_>-7h}|57A@Xhgh3=N;mnbp)oRthm3` zjt%OrCM>!$M;vfdMY^roLETgFPJX0)^pM6+x`%z5)xYI|WI_rPJnb+H)>eF6gNLa5 z2eYc7p;ZKf9#10|-UWa&e+{T%xR>v3bpYljrRWjB9b?_xxeFlO42+7mMOa^>nnj2YY=C~M&Es5XK}n-!86t(RUwtg~8{Oy_AM4wB-*nwm7m|wi z;!crv3%`d5c%LPc6=p}n3wd+9K>8QSIXCFbV9AA8sLhHcl2!@?R-cVbMz|WNdxh%? zI5zR7gx_!cyH_ds&iRL4{ev(=q25p%s;|h;mcfJ5-xMY^KR$|JffHeMwd}GL1lzpV zo|^nFuo>t#N%4}G5gxtg0e9ctA<6wax+C?pe(3Fa?z9^HHKYWKvMcZUItf$aQ|REj zsQ0^>GF5T$pWALXF&s9BKjjc}k9KBAhv`d1obfABm_fiRxg)B)%(!mjaYaD99O>Xu z+ESG~1LgNAla48G*M<7Q7OK9bogL#LK){l(+gSwdsDsb)h=gRR*F*XWjPzWuFF24b z0?)wU>15>BN22qkc@ZB$_m{IeS%m}#X5b39Ytal1`7vYCB?g2rUNIUM|8#Ftn!M{I zXfji+d_AdFn=ZrxN3GH(h5)?wN3X5E%Ezg%&GR3 zY1*IO)Kh8QHT8O4P2?ana^Vn;O*>2BQDY*>_|EG{)PdbL%H27%T8nJNA3FTQp)W^n z9u$}#N7e-SI8tEghz#!dxY>$T=S8nPavi@yN&6)}K1=-Q<3*3;8lHGdJ`x!#H$~E~ zxn+6rzi|5&1?gG{Zh0ZO&qH*|=ow#(Kc3=3EUS)9G3J1|Me{Hn3}$Ho6cx-)`D>`# z$WbJ*cHF@B>f>oG<4vu=?txqxcj*_#4*xXL9JiJB%64AZ&zKZ!K{dPi=8NpkWhRb) zf8J{84jSJqQn#)&eXL|@pBwQrr|-}>bfa{mqap<`>tt;l3e;wFn~i@z>*RN7^-%i}DqdpI~W1e3`Jtp1u}D5yg-2wA9-pEj&Q8rEkt-;$RX;C)g- zpO*XODC-O5*_H3B)h^CPvke?(k5t!XW4u^lJ6rpHHv}~weMsg%4d)0s z4cQWDxY@t#`)Ws@g#6rfTS|0Da;q1y6xuZQTX!RLl;idZIe1GnWZj3q*imfwcB`JA zeA{$SKYc&@w4i}!C2mHCbo9HCtf@P)y+mB&TCe{i594;C4vlj)7%@s%*1jnzfB zEQZN=zJ-~DMm)jWf+%4}h?aoMVa#PRgdDl>ZECqbSpq0O zxY<`Ju~|ZVg?Dx^%k;jp=`AeJUmZ&xm)@9f3tK;L-uDi%EWho3X)UMN<~v%Z-{Kx| z0RQ$Pj8Ej|sjBpW8u5aAn*ld3TnwCv{QS|rq~wnPDOb^_6-@fe(LY)uX54pw;=R*f zeV?n*7$xMrf)m!`YEns0@biPJp<-5vH`!LCJ3;lBHU0jVo_?Hq+#+-paPE%>Cppx7 z59V8>oMoLWmF)<~wUyYm)gIZhH(>fElv|o*;dP`*tMB5< zov>TwQ-}f*vNEu z9v``Hjz{_m^0#sG`xarJPi{eQ;)n3}3No$i#J#f$J{v6IUM#2$2v|OVrPM$B9JkfV zrM2tFw-{r_Yjhy$R3=)}($Gv{Js44d*Kk@FIi-l?T6j|iM~N*T{iwOHy!xc7=uTL!(^e4FXbndtT>b<@gc z4=qX$dp~~ceN?Feu#X%`>*<35!D#LX5i<4lQbtX-D>Gl?Zn=51{|9-_Y>O;3!)biYKUJc^MV01iv(EEaZ^ub|tzM@< zdQ|Fb_}A~))B)%<5;BOfK2mhd+&bpXCHvwRGhV5an~OSeKI4sYb_Xqba#OA|?wHo% zbg?(3z0>k(Y@3MQvfhV&qj)`B(tzzRDNo{gG0@%pg`vQ>gxRR4gI5ztM_v-R7*BoK z5zcVACbvE0u{$R`P}5y~P)=&o64`R~oy8-rA|)Gw)So6r0( zU(f~!O&t3>{X&uH&3hb1M$eY{8qP=GIY^(Frd(Y`W^ShP28q_L!~**#dlMgCmP~8t zwje&XwY83a;Yg-#l&9B{37#B?D9N|$R^*Un7Z7Cu90DY7G>F~zZgDSpsiWgIFZpUY z+QxP7qzOb*gniQe-KGk#KbYe@07=AzYsFsU?Buu*h|8MVFT)l4nAzb2VjoEQe%wdE52KVZo4}ZD7>Bqy*N~@O7u5U$F z9~+llSG98aE!@6w_mqCS#Gr0NIrLyltxW70uKvb&LVKW+S$U}lEVx!&XGsz5&uVZ# zDUdJqse`7a)>C}2+6^axP9Bl(<5UjUdyo6OeaY9;jcz&TkgqDMQ(r`$(rM4yIcCb| zSdEOq?jI5LQ?U&gXq=zY$=lButDJ6&=CC5?K@uvv2>Ob-EIc((F)qc}4gZ^do0e2QGb>2MZ5o*6ZF8g=(>>3Rcg})AxNfIM4 z$BfOkIEYnzFA7I*{rt@k4>O=fTQ24rsDnNtVvO6)Z%30?_Bk=4W3;qZRxXlOlyc_+ zX|c7HwS-$$+V0XnbjeA#Ehur@HLWLHzUC%D8Eo8ySV6igT3OnNesn z6AN7R&avmH2e-fm6*x)60dIaa*L^wz12q(6^g@O7TNKy}TV$w(l={8$tohwNYVE4; zr3n^0dB+;HFQ2O=I6DZG=c%R??Yuw1gHk^SUPQUiCR0W(`xO(v;3=T{JeQBBsscT5 zoNVA_r9w2v*n2{0-RkRWok^P`K-_2?YVPxFOZ8A+yGne|HT*(NQ{&AArR$gPu(0o8GzV*}r1g}2XB82EJC z73{=)5hW@R%3V1nl9QR-{P;y89xNPF){u{zr@w`0_}+L9I{57FAdN9;!nzlY3s1%q zbs=)UXBK}FtR5tRB<0?!c`}&8{6uN(Z#BjZS^D7Jj5&olqZ>eL(!{wW z$;kdp0oT2QaboQgZ%g$C3D=@{Z{P8q0Rlo;sMC|du=vtGl3P>97Z>#tbvDGTVl&|> zmv?9db;Vm4Jp?mGCTzVjZ#a-mZimr+XxZdSmXI+n0g81}b9FMEn*`7Qn@PHYMfc-7 zu0=C5>2~&^0=7Xz&bCpLRN&6!H$v2f*RsP`ljXmzy2Fz3E{M)~GP1joF>A;EC@2FQ zTW}Q`sq~?Od&U4sG>+FPVMxEsA7#yuTAR;l6f}DuTyfxak^6Icu@k>eh5Jwpo$6Ew zMI`qIj>}t`+E|H*&UHCU(4=zfL{aEE5-`Rb#GlI1CjrU<4=CIkdEph#J+0c#&NouZ z2y4CiS;2#pGSifKAldP{_sE500G|-1I{#GKziGS@O*Zsqi9unnd?y=E`SbPOz8_XO z%IHlUjmVbhbf1gPU-Bbq=|}U8Ro*^GE=naB2sPx$;jI9XQlJP-K8EDe9!26$t=%)0 z16f=iIET~DLf7D1?T;;oy&s&nWRj&8RT6c;KFEKpMJu;SjyYeZ+nMo!%3!PZhBfcq zJ&KEaWJQZ!SJ!H0u6SCwFASCnQWYGVyc1L)Wz8)q&Ln5sZr!-|liuW~;tqroL)e2~ zhuOy?t>}`#IO?9QFi!{-`^T3KToq*l+ZL{iIqhzqDeGzIhWRml@!-2F#O|XZs>BI) z6^M{tV$LjbI`jHC@h3Z5U}>)Jm;~%{f)UP(2~+L)$+yW@6p^Y!Zk+#qcvUXX{B|X= z5Zs6nF?{mMjniyxlB{V1;C~VyO}1VmB+1hTHEthB9VMCAO5d8csB z+(hSc@t&(31j)_4;oxC|^?QdOtXm4<=Un;7H}N>>T4du~-8eQ1U{5=gUynG9o6At&jcZ<0@us zQtt1}P*dMz;xnozz+<0gEzHA2J;3|$v*@**$L0aMbvb2u(|jgVQ#tIBaRuSp%G zhneqOXME$2zF{!|G7tt_P7Af)hGK5CiT@FdWQ|9v!Arso_dQ^L#u4Gh*K5aHp(;3_ zLmHp>>g$RK4D8&7S@If&7eS_~Q3(?*7Wxevv02&2??C+f_yyUO9Y(6%N?G40NQxCk z?@3au9OiG~r=aA{HHW-LMp~Xvr*XLTIUXPUX>1YSfE*Q^>A!n{8|6{?gjLPfONCp1 zE)0sT)Ai%e7;43l67V-pdBN7$SUKWW-Gm@fbCm;?+KoFHb zQtf8mo8_$ud}jU5l(pQ_`G0!=Bfvbo`@kQii|&Mwd^x}C9n%Ck6Y5mFxkX1jUNSY8 zzHQ*(;~WVz-Y@ijmw61fe2mN`oyJFz!k~Az^i-@*s`Dmte~?N;Y~lU51^lG<$4O|y z2vorIUS6etUZ2Z<#|^Efr&TO|S<5L(0UO{iOY&Mok%KmPIf;`RT|nbuBEyoWRw5HK zOa|rT)Jp~8Ifupn69L#dz=Qgb1(5SaszBI=k1K6d({eo2max8wCk?TjL(8-qXXF{L zOkhR?9{@q7C+&E=W5xibj8jM`+Ec$@xoqyjB<`{EH4X;iY#8$M5emY`R=E;lZObOR z0hhuKkt_EO<5NDQoOY;m#BtjlGp;^M2&^@kF-H7&jaQJv2<-mp9BB8hWUcrCf?Q%IGBc;)}LAUlNz)KTn|9Qp-Q1mCb;7+h*o5@Q5;!*LO`kx`(w=QG#XFeg> za4PW6k6Y^zX(zR<*T#ekUYip&1f`_IIzzu?=0BWeU^6@sv(NXGzj6*s{3p#P9&V7%@ANMn9)qfwu$vd^(p+S6 z9V8KnXQX@`q>&GD#lCvLR*?Buaxy8QcDkDxm|A^d{_OeJX(@%4P=G$xs3F>tha~@X zX?fA3%-MobN)4M((`;u_*vx?~TQx*P7Bt(+hYq;kfq=572u9}e3Z8~vQF{Rd5vg1v zx4uP_X{YpL}8ZV_%mE zU`CKJkTzlU=^J=<7kL~r95A2HcJq-3f@cAxAUCZG&*wSF>C!OdkZ%!@c4$mQud0vY z(Cq%^cz4f@o~cM`2W|vv863|XXocksv5~t3leq61pV;} z!efrd7J*dgXwGh&x_H$`l-cz^(Eq#W9PYzAjX=OXsGJthfu^v2YWyRj1}wYrryn+ zM(?f28rXI|dh+(+jR9bYvf9g@-N@$vO%LquVjJ=Mz}WfSjLr;z`ZX09bUP*8xD$Zl zFayD0TG9~A%K&qP{2$gg400g<_pi4el&t!UdJb#=;7;+aG4Y>COqtXwJQX}YK=vcA zRH;T75Gyk~77Re&NRBs6lYmvUco11!o`Kg2);e|Bh{}OX(oYP2k)$%mRFn=*8Wb@v zJ>wZobwiMT4tRvHaADddb%-?%9^h8-X_)xrvN2=aPX-+(!YEdRcCk9>pHTusLv9ek zL0ZZ;Wou2I3_xJ*L?W^pTFobn*9YVLu9sICn+L}?I+zH-7o?@RBFxX?t>}}$avAlG z09(Mb<8v<2djO30H-qO_8O(~%;YRJEZvpipI_an;R}SN%9L6dBoQ!WU`_{G3|2t4b z{QDo`Gj7rQ06Z`LqhFNC7!HQN6~jgWV4AKD5!DWh;e8pxV<3r{u)zttnIpljCd^G} z3_#!x^>i883d7ABrWu$+ql1lM{Q|#C+~KAov}aC7A_KPcsl6(X!Mbr0@CN3aA4X<} zkK&U9yH#6D{U?}$#gYBi8z*@jcn;*ocaOR6??L>Ldqjm8sDMB2=Sa=M(O{T1&D{Ts z^Y>bevn~Xf$p6@V?0QtHwN+q>`7D1Rd*!(k$Y1n+)}?Q8+|V3#cf0m!VlpYIcDlWp z?{mQDV~D#y&9gX19sriS=IE6imEjSr>31GSe?IIZbBVrhFJX;#hASPW6dre2BkONr zgYxi5QVvRkYHtg0x%XG2`dj1WEqSt-I_TyN8a$zIrjHh#ApyCmR}!Pqx3BenqCt4@ zPB2Zc?Tmj00buIIGd=+em;?38)7G2f%kr(`>*&^y1>k5AG2WB}Dvs2rYZoi*-n=f} zjy}_EsR!Q{(Qi66@LE2N-t5lsO;~L{LhU=Yj`!BeJOK-I#EJVRNm0LB9l-Kc_+Y4s zR*~NH?c0EDccf2UJCplm)oHuEe|UASyv%Fk!|$!D0exHBu~{Gqo+AcjWk?g920$`D z)z28B%h*ls?OgFc-7EK&LGQ*tBJrOBv#buf=0zs5uGcQ^*iG4tS;p%0i#`D4hJx5_ zR{aw<$N5|Jrmjr4kN@f~h6c!^lH6joKP7>^G4dMP7aMo666%{Mjn|0;Tl!uZopcZS zdT1^#kVVt%BAi$O*#eJtraRZ4NkOe%kE;xPijdBl`v{oX&-H9Xp10J!@AR|J7+<`3 z(XH8(LyyqG{B0~e&b05+{D!$+8nN9j?0B>Q8lOSem7r5Wxy9k$|JVft-Yb* z#^O?MJbnE?gBjarBm@^a)-qs+wpbuyZ(r){ZK*V74+Wiv_dELiOl@a0pk0heoA7-h zHd(y<;MPA-kBNn8L%^dBErs2jUYX=F`CfkAEj}+t9nbFjjo9sbm!)M>y&!8g1Di^I z*7RKj?E8ix6CC`aTCSI^#Dfr9?V%kh`;zIvL6cwAqv>@-9GDGx!~%ewP3;(@WClX| z;y73Z@OS`nCPN;rnSdonFiUO(zJm*Mb81_hk>(#Ma*>POypBeaiIPj$Y;eJnz6DNva-~VWeuD?9mF<@g zFyK3Q&zv)#E)rykF?Yf+Gr@@zn5&xT_+tcH-qCHh6ncfzyJ%jJG!h>+ycNR1sUTgL za^Kf=9CMdNf3ybi;#?N<3`DndXAOL)~GZl%Gx!FFq9t!TLk$K|r%DG|i#p61fGaTT+yQ!FodhPneWnL2f_Y#HP*|+Yl zoTkzD)D(@m=r)OWh zR1~6>xS8>&egbyP&Oae*V*>ABF8e|w9^9<}ea|ZK`N&udPx5c6urB`F4ICIjb%kG? zSswh~fC}}0T?N+Hn8eM+a-Zj>-p9>vg7<%FX>bE$#nEwKtKqhwqyaI8;{q47JW+m2 z>Acqrx{Pd@(Ya%|)nXihYa^9niED)&b+-SMie)!2r|NeMzMn*Q1 zA4V^#Tk{4ASt?sCPiyQF+Ivv-_yhO+Q)ZnLrq~PxY@yreqw;%UkY?*Ka%sCu?Y6T0 zj%Txl7?U0)KDBY8DUIUGkN*|e=LDf4D|g{uWH<3nT)0}>kqy2w7Fgi-xu9LRTq5S0 zJxr||dcTMI_0WNNUW#(|^3PW+5_QB7sg=p$WY&UZjQeSgf%)YjuLBxOFZP41?5Vmz z9(BpFh;p1c?bvZW_*=U<9qPW>b9|FEKXbF-z4@V1nvblw->>@)SB>4OozGQ!d55x0 zYs$lejBE7CarwzEH&aM#=$BvZLel2EZa=<|`ei=DSu)STRpYEghaAED8&pfhUMxIe zRnx9TY!^O@z}bwz)kXfhce19wZ{zsWG;G<{;cff;p{CDy+{I$km&oGj`h-1ytA%V0 z0?n7BQ&tJtB34}U7%6+%OD<`*BOj-E@6x8_sJ}89KBo+OQW#n#N`}lz{|^&R-nmC} z5U@mLw0kJ5^jJNSNkm&zmG#QT-}EQN$ewVwq`&#mCys8g?dL1{AG;|v+s`d1ku7#P2v|-WrrizAPsmx1C0zQ80bB31!3qIXhtD@d#&5+{*YxxD`*?V8i z_l8|+WzxNtRvG-3jU^5*RZhA-n~tbtWKi{n{Rl$YU8PR>%20mR8o^cnRfrsUi1fsT z%7FTM1|Q=I<833aT38`+54ZId&y+O(;331B@K1jT@_X46?e^I;H*w)oaX@S5bomP` zx;yey-6bY1x+Pz9;pW+ATui))$EtwB?9efY^6WOjUJ{X$~^rc~$7KZZWR1=!*de9&s#doUn+thA`l|4kUSb`=`TE)LOrDft*1rq; zwBS8MQYWaY-G| zd|(;}3{rT&MoAmHC$%5HoczpLOVQ54b5TKcB)sf;`*G1Ig*VtFU#sQfb(G!(&4K52 zAiwo{ZNkZRHrb_zV89~4jAMvHha3=se@d`&2gD;ne~N5 z4Ow~FZO*?cd)-0Y!}qYSj|%uZ$$yx>J0i!447Tp-qhU>w3~0nTqG~%5UsB~-IO7Sr zNjq3x>GK(G{R+OsEC{Z2aFP5hjK4DqrmR{n@%R!2YQ){R#4kE>&I!(>9^a<^dCGqr z3yiMJ7;Z&Hu_?bA13Qx9tq5V#*f=n!--Y8A9nOQG0?6ZEzd1lvEmFa6H!<`4^&%P26YxjRB4nB+c5LbTfynd@t|M@AmumdYEGMcuYf$FKD|Rux!1f!94T&v*9S*2;n_EWH{P3NoC z2kg%v<7=;55B1ljZzNnj(8E!=fK&AH?6QZs>wNTbEbZxn5>SGdzS!RKl(bq}rF0K= zqGIP@Bs#^)d6z|DzkRlGKX36kbp;Am;PtAEa{k@@5&C}5LI7vq2Cn;Uzlci|*B`l* z?z8(wC!`gHCq|QsxF!}{HWI_|DYZ)CbA;QQOcx22*TFP-9T|S%`Dg%K_wV0z1R9kp zZ4^8I_$d;kqlkxLPf~lTa+v~fY}Qe4l#G+^cJnEKwES@Lh#9s%|JOk! zejkfX2fa?>wjS+BbeK|V-``HmRdAW(u;;lq{K|eRQM%6^2$gwCp8UugSwPai-6|^W z*wQVQaj_Jaz?rZ|PWQ2IvRdx$-L653eb&e$2gD%WR!xf$r^h~gE6ILQ-G>*TDaddphn8|?Os+QtHiuW_ zL9-=R!i7zJ)l8_y{Z6}!a0Y-XD_qpDN5=^Q8r5AplCgjgR($@Tc47#cC9p$@W#7oR zypH*=|%cw;+C@A-54$+~#Rp1K~Vfgy8ziq@Nw-v^g$4 zoHM*{tB;ok-v-LZJNjH}rzLK<#%M#?Ea!cBd-BVPtv{`X~SZTa+h5$Ffw8B3@J&VuhiqF;& z$E&X;yAUPOPd|D=>NZJ)f*qY$r|qLEFKuU7!#YfcXjNuaoa$*9f>wCY}) z#x_K=931}PiiGDzCV-PR@NX68oLBSA-tOZoU2$`9T`Ia)N zw(w`(4c?;r2705R|7LGVM^={^PPZ&No3BNWyi9-ns)056IGId)Zb*ZutUJG!rk~Q2 zT@u@o0jySsZ!|RSyq8}?$g8sqASbF~!0(KpBQCI17FMCwE+JWhO|d|aB{~(B%c~R2 z$liTX$w4krcRi=oYHDV0WAlzx7YXH&pf##`KO`urup|CaVK4b6D-E?USrZJ*OmhU! z?4NsgR4-!pdx^!7_TmBrd`iIDMnKFv5K(MGGV*KOfPL?M~$?83!lmu)9 zog?D`+Pe?KL)U7ErVEG>PdFSpI=A;lDXV)49v=S&(-nH&YsR#%#e?ET%wuaXt8XLW zY!qn=k=;hYzuk|3Ee?Kf>Hr-tZH*O;65A-FpC|jH>c-4?hTP`kLPJpQHF0_R#osfo$h0&#cdX6lIR72)XBF8u2 z?4aw+0?Ol6kcrRI_N!Qe|CD~iOGG@<`06P)#P$2Ro9{d3EkCq;Sz+#rmC>J;L4)Y` z)rvl>+6w~eAw0d^CGmm*TH+-ek6YS`WwJmz_;0RVXzGRJZ`%YA15=;K4$o@KR^nl+ zL{W&Wd*@YChnHxiVDRRKj%k5R!KJNF%J`CH9*eu8%SQoet{@NjH|PC%^r`Fi1JFP< zXuy@5@Q-O9Q5H;wVmK+dukn~2v0+MuEVxuVKDV^I_%AtZnob|vNv+FHsRnXF-|$-` zVlU^QDy=j7J)kfto^-z=>i+RxNw%V`h=7rfY96 z9NNB#c*j@p`pfV$ef!AN$^=W%V^MexNFQIheD>warWVwlQY5zL=kK3g%AL2FNwaPO zQ-eV~-4q$RMo@l8{jKe$h{Yv^jp?;|Kx#np zK=bO6lh&;6*~n^yH!_flje{};SX)&)R_Xd(u+-ldN{;$85o%B_B=#-XD-P6s3yVxq zN?MbjzXQ2U@E#phrE`|M2hb0@mNLz%OMLf>C)qZ1EQ;Qo_=GI5i%zn7uGmS2fvFgw z&Y6~i$NwC#n+>*Z-ne>M=dxHI<$kiHvJoh|6g!rH|KW!W+g_-)aal%tCboKPm;XdX zPvwMPLfX^$r;U>ZHs5tL7U3H+du9T?&apG*hA%0Q3p#T>!8;o}obx%>mt62;;9*d$ zNmg2ss}=7F3g>(Ccv-cubDwS)AOq#OG5D>2b7?RF*5v}e2p*T+@>7)sqcC2*+GKHN znlu=1 zEtk&wubf<0l%+xiY>rv4HW&&}`E=+)T2w#r_K(>|OdPJtfa$i9Xe(;JJBT?nWF&m}>mt^=W_U0qC}#h0}97w?!pSvFgK z6wHAtqpGT#m{W4e1{Q9C=vqauX8vQZ(pt!yu-ga8;t$f6MHr_EKYm%z?X~&_H7Ue!GvND)dRwb*;yvV@=WiGF%%Fg%z5Ip9e6C_l=-)+1e)L>(EUESA}(^9XO zOIE&b-uWgKTNu(W7<*D_>TK}k=&4iWBYvh&dOn)Z+_XFWEdY;QIb6_I5iN$l$h0wU z-=*+&S@3!eH@$RFNp8IGMvj0IRnAqlIiEms#RTlj>xeL+)@Bi57rNe9`~_gt-Uv8 zu;$~(MVq(FCC#L>|4$H=8Las>I#%>erTDAT-dN7ZG$m&Glze$&``9}DcNEd0(*>c) ztAxG)f^N}1Y(bgQ%`|*PXbWq*Pfbrn1ToIX`Ex2It~@b62y>_h5Igm$8}X z$Oz9{H+*Pv2|Fx5Q|N#ZfEp8%0ZhWtgdMw#i##NehG|81bpD^d@6t-Qo~BHYDO2t3 z2Yp6vJ#B3P@Nx;d1M3KR`^2ERcqqK$25` z)&503COWB~iKBsSjQ+;ichgqxe&lV#xK=!>iA-B6v4ICzKI1|TW&jT4mkG0K@|Nw2 z3LC%B)V3RGDLGWXMSkyxIMj1yoBIVzl1n9m!cE!RiynBvfFpAOH?H=HbydVAS_fja)KIHw8 zJl*!a{#URc|2};KKzxXj&~H;^x?l+ z&;Zl9@TH24k~Uh&Ny*BCrT_bFVb#{E)LRx?3EG4us-!9!gIbPXz8T~W+*G6nWr4@5 z=%^PfI~5lK_DJ3f+l2QZ=y`R^$>0;>FXzIt_fe~wz73CQicA|*r64{Mp3&a1N=D7E zNrs!u|3XX`N0lYrJ$w3QpFC6XpJ!~$hw!l+XlX-WU*~|~mQReYtpCTK11vkHy|ke)aCmw$%)nM&G`%oU(_#Wxx9)F8 zdpuj^%A+n^o8^;;6HnJD^!v~g$&a&duGSS)IGMlhY3bT&vyqP3QH1!qK-T{lRLyXD8ZHnZ#=6|IsOa>?3Jt5paQTEgi%pC)} zwKgA0bhw=9xO}2hYSkkLJT!v>fsi2~P_A}1mR*0-psoe)u4Kh-UvEhNYoGZX^{)f3U2?Dh zJjYfVf{7p{<}I7TrXDS8LM|JK&*_1XQ==6BE0BjM6CJvyZMyR48DsHfT2+hTs`Gs? zUMA6>a$b7#;6|Dt-I6eo!pvq%;auKJd*M zvY;UdN%Hluc;)DcD13R#fkz~NkBgTV=59)-Cr?pF>C*|b7QPAZ%Qk&71=+%fFE*1o z&hLXn*x){Z92Cz38pBnR2n|ndJ6BN;!6cZ=G?Za01zS9ly4(u&6A5~P;Y)D^C-`1# z)4MERPi$FsC~%;pW;p@?HoD&> zt1E!)*!73Ux7Ty=BTvM00F|%TsMtvWN49D78{M-!5fCyFYcL`UJbx}?@cwSEI-GrX z#WDEtUr`hV*q}`|>{-h9VtJoc@~|5$R-B#}RtKiXsBi%_KNs2Hr{|=Y$S~sUc<2^{ zaD4q;b;vtE$FY^#r7vqtJdV?iQ6H=CpYB<4TD`ezKm+;aONkQ9_0!LF9mu^glqDCQ z>)}f&<3dl~kMr2_=38ff#>i~nIjV}C+hHovnO5lhD~BzNs_d{|i*M4O*`W+ntfRu6 z&qgEC!QyIqQ~+$%MEo4}XpIlKb=M^K8w_D8S@MP?WsHONaB`-ffEE{l|~PQqe-vMS=RFj57x*5WKp=ZDLUr< znj|nYm>^9nJYZ8iGNS>*jflZ5ty{K%ihB1<<}jns{5-ax5kv4Ft+NvJ-X| zX%J|zO^*VQ^z}!S5GqpIwG0hkrSF$g zn<3FSNF6k;jgt@XotYD;J3kaes{=LWq)m?l<4~(L6JQ1;u;3N_lz@%I@2g*HBOa_d0W-*c0KRkHWs!sU{!r|# z4}u`8BimN*O|*}~AwY6Y2M*D~nq&zCvQ?15^O_n*Nrk+nID8ZW0{&&+Za+m08NB)m zI}o_N9o6ZDm?r~jti88`PuDu7tRP6aR9YZ9xF%g?%HE)d0;vBsmnI6n;B$oy!2Rj+ zyI+y*s4?vXh`gE`pgO2fg_wD0Z^r|LKi&XfPI#f_0NC`Y>kpF)JK)fr1i&kTZk_y7 zKGg840RXG@D-PxN+cJ5t6r#W;CK3<*Pf-STp!5VRE_WwEz(Bv3$uswjsWVrRkgXMbO4;#M{@uZSz_>IN zsPAw~WtB?>HSrEaC{^xPJ+U%()2h2|L-w8k0j=jVnd_?YPE&c5zWTeP5A(BE%5ip?E2n( z4(3<+zzSJUBZV%^fREu^SDt_t((Fd%wPU4Q(PUDY;AZ);#q(e!*erV?n~N9xc~)@A zNZTJp@H5aGz6s!`VtKuJTgqgW0N`O@tTgyo_~LQbTkaP>wr?^pBK@TIODx1=67Nol zE%CM7BaTz7ixZ|)VMV7^I~Gsp>6W`<_oz8e|3eqw3)u~QpTtAY zgPLS{8?$7Xp2yl2E{afg_08?x@ZKpvc*mp}%l54XK76kBo^CAj)L&_9ttz3;jD`8& z{+w1VJr|B+IW2hcpom9=joy7s2$*afGWqhd+Q0>>iqm+lM&i6d8*dzPz@d{+XCc)l z;$AhkL>6&|9bhXcTj_5Z9+la}lO%EMA(fJ5*4rPh%$y4Cn15it-j51Yj_xa)VgIn_ zw&om~p{BGLS=n>Q;!U6a^7tixDPjHe}DPH-)CXQt;Z5% zT4=fg7&Gu4+v@+`w|7-3no_fRD+P~RQ1>&FXIsNg1v>1D5DfOJ22PaDw0>lOqxqsX z*AuPyz4<7&Ij0rQX#U_#!j_Ffw2AFG(zVzfO37?tS&xY1mz538N`)*_BW z2}(BkYx~ks7OsWH?3Byq^mr$1xWF*xe9Kt?zgo~@Pfb4a=_azIp)$ChnYq?qPUlCp z7$LH!$si(>{?kHNJ(w`**=piz)Q_@3tq0-*2fris*7n?R;4XIEzS-!JYUv z1b2pc5V5g`;c$#P0|`6|N1yDht0`*OQZV5P*3-?R!R>-NE6b6J>;>?$W;bU&KU z0Bf`<-LV_gJX!^LqmcqQ2lq(n>>cO)p!hrA{rY&~BwKN3<74Q@DP!2=D-?}(LclPx zaQ}mL@z#~n^PFmBJSh4s#%ENrp)>1R(5Hr$Rt_$?XL7)-7Yl%U^fo%1BtfIbQ(Q=V zP$-3sb>fvZ`ZhFEJXy171+Ae4CPxzd_k?~gT4``bEftjQY-z{xOQml?0H=z1h;(cJIG1eLBK&0{sBv44TsgzrP~6IE~${ z>5@}&I}snfBxX40P^}TA?vAl%v?37|EM+`wvP&rba*2>*Krd$P#V>C@s!}m~D0oGN z|0fq8In=vwBc}M!`stgfuV~srmH*0oeoo3jxZAmXHYH#j8+9IgmZ-7fcb|5?%T`t7 ztyYn!#m>3mb-oM$adAg;uN*Kn3m<3`yls-W(Uy~juC?0kE>OY*sRDI-!ZCG+82Wf@ zIon_Z@>y)A+i9?y?iv;daq|IiLxMrHX$t_xZkJusQTmQ(zDX;{HBmwKlw~ORs2i1( z1t)Col^bgexN&s818?jH)Z{LcSAlkm934ue9LDEi*^Ffn>fd1GqH%D`t-F4^w#(=F z@R&Z!Ip2{NvBfL9H<9o5#OobH(bqaU)yoe;0j1A!z~u6Ph41vHn8WfnXuN!@h8|bN zo0AxZX!SnY9nm)(6qPCAs= z)B7ry`Xi4B4fZ(#8PM|NyQc07ZBqCB7H-}ok7Mzr13JG{K;;3eSi||fz1o?wJhl=e zEa42ihs@fFsX~8u<~{6j{^m6Fg{CEg$?2gl^{b?_v%Q9zqi|BB!AV3&Uo}v)W_ayi z86iJ8^uNj!!T=q(zIL$slaF$W2o!Yql^+D@!>1Dn#rWVhr`J_>t9wyPu(Lw%Jh~m6 zubK=g|AtP1l{rUP`iwX^Sw*feGJ+KaYfa|oE6$E#oDK2^I!BQ5TuUfRdSX$rmx(9} z;)B3;m1=eGAMD-ng~I2+LEvefC-AC>U8J=_3Kx^(6Fo-*6|s*D^yX4+_jU=N`{kW` zOG&a~h-77|TO$Lxommu0FqgPyUcC4Vg3RhIX;_hQ|XY#A#g&{Y#rVhX||w!~i^NkO0CzKFuh9-I9AC-mdMq{7ZzV<&OfA z85-kmcKymtEK<_YXs+V#WMfO8BncEA&)~T|`%!-$hOd5Y8R3s9ew-wc`GYrzS1X#) z#sfpZKIVdt6%9r4!Eb@H&{V>u#R$h@y79he@X=WNmt9@WF|iA5M~noMg|_wL$<$@h zAWo*}Kg7Zy;rSDBIHDTJf$T=+l|cV~*yz&zxXmE?|Cv&7bAQZ%FerJEH*aXUl;|;8 zXw9$m%zmgl<0CSNIq)5MC}TJ?L#3n`YW!h1?rneV|N3@K(@%ey(^-Pl&vQC9)i`<# zS^d@oU_8Si`tag-DtE`X%}TpWC^+;5#Pk>%)J|dkmLDnUFTeVE;gj9ke!BKKRrT-Z zxWjvDa8Y%tnjW^os7_AA4s`g1A4J0qmPxttYyJ7qFSdqT5N6b=Vnn)r{;mSGCw+Qy zw*gUQqdVSHBog7;%w^E49+;UXr;~%Spl1}1nxqsFC7xNnZZS~Dms+fmfDmVUU^uH& zVAOr^6=ml)Yh-xS&9FJ2Pg#((V_0a(q56Ga*vk*FzZ`v!2#)t7`M8&z-c+?pq2BV5WP{LTmrZh<`MNPv@v^Fhjg}Iw}UXI(Z#>ZU7FW?!4FZqcLADN>G$Hz zbDyFIKRCP=&v*>fsh#CPqXE7?Y%iskevp~;og6Dt+x#*>8m#{dR(+lJb{0Vk4YdfL z`CJ_+276CYVWo!;^t8AhFqKOkAj%Jj)Pk4HDd@r?_{i%EBtT#H_@5b-^Q-V1@RVts zxdI=3c3#hkGZzyfXD)Se2WzpocP#bL6`wHKoU4BJEHI9zE|_24&|^WWRHs@1rjXtI zcF6-C`t9WGfE23r|MxH8*gTLPg0u^|d*xPXPcw9#OzWBjzwrLNx}q8V1Y_X0EK&n* zGcSeBHcBp|n!671xor0FiJRGV10r|tLYIXNg$PgQ_Ropphz*NvI>@H0A%L4LQlk|B zY%V)Ctys(#-p(LplR7L8@6WNZ@72`s_cvc}`TmI@{xFFDdniNAAPsUY@BTF9Z8M)0 zA;0M@>(je{@)RrJJ3VVl4Vcm}C-*d8tr<*iE|-bYAF4@0eXHiMvP@oL?G}o9%0{7^ zw-zRRmea|MG;SgX)MwZ<06=H4=hZJ&wuQG9DsR|Rn$3;AvM}>>xY^bwX?=O9KxO(E z#uJ!MH389-1Yh|GyYj9sh#W9gskVlM8^%&uS{!|gK(2i5wBkuh#pb;_*%UI6glYbX jvi9t;0t53X0-J#QWS_$(oi|t|8_?G=)~>tj9QXeKcV0=) literal 71995 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