From d21d045eda16a8daa4c61f6e9b6d4213a8f62532 Mon Sep 17 00:00:00 2001 From: kyori19 Date: Fri, 15 Apr 2022 02:39:30 +0900 Subject: [PATCH 001/104] Support new signup notifications (#2357) --- .../32.json | 815 ++++++++++++++++++ .../tusky/adapter/NotificationsAdapter.java | 9 +- .../notifications/NotificationHelper.java | 15 +- .../NotificationPreferencesFragment.kt | 11 + .../keylesspalace/tusky/db/AccountEntity.kt | 1 + .../keylesspalace/tusky/db/AppDatabase.java | 9 +- .../com/keylesspalace/tusky/di/AppModule.kt | 2 +- .../tusky/entity/Notification.kt | 6 +- .../tusky/fragment/NotificationsFragment.java | 10 +- .../tusky/settings/SettingsConstants.kt | 1 + app/src/main/res/values/strings.xml | 4 + 11 files changed, 869 insertions(+), 14 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json new file mode 100644 index 00000000..97ad414e --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json @@ -0,0 +1,815 @@ +{ + "formatVersion": 1, + "database": { + "version": 32, + "identityHash": "c92343960c9d46d9cfd49f1873cce47d", + "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, `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, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "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, 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 + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT 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, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": 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 + } + ], + "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, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` 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": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.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.collapsible", + "columnName": "s_collapsible", + "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, 'c92343960c9d46d9cfd49f1873cce47d')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 9cef6245..936f20a3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -226,7 +226,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { case VIEW_TYPE_FOLLOW: { if (payloadForHolder == null) { FollowViewHolder holder = (FollowViewHolder) viewHolder; - holder.setMessage(concreteNotificaton.getAccount()); + holder.setMessage(concreteNotificaton.getAccount(), concreteNotificaton.getType() == Notification.Type.SIGN_UP); holder.setupButtons(notificationActionListener, concreteNotificaton.getAccount().getId()); } break; @@ -283,7 +283,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { case REBLOG: { return VIEW_TYPE_STATUS_NOTIFICATION; } - case FOLLOW: { + case FOLLOW: + case SIGN_UP: { return VIEW_TYPE_FOLLOW; } case FOLLOW_REQUEST: { @@ -335,10 +336,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter { this.statusDisplayOptions = statusDisplayOptions; } - void setMessage(TimelineAccount account) { + void setMessage(TimelineAccount account, Boolean isSignUp) { Context context = message.getContext(); - String format = context.getString(R.string.notification_follow_format); + String format = context.getString(isSignUp ? R.string.notification_sign_up_format : R.string.notification_follow_format); String wrappedDisplayName = StringUtils.unicodeWrap(account.getName()); String wholeMessage = String.format(format, wrappedDisplayName); CharSequence emojifiedMessage = CustomEmojiHelper.emojify( 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 6b9afce1..63c17082 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 @@ -16,6 +16,8 @@ package com.keylesspalace.tusky.components.notifications; +import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; + import android.app.NotificationChannel; import android.app.NotificationChannelGroup; import android.app.NotificationManager; @@ -73,8 +75,6 @@ import java.util.concurrent.TimeUnit; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; -import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; - public class NotificationHelper { private static int notificationId = 0; @@ -116,6 +116,7 @@ public class NotificationHelper { public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE"; public static final String CHANNEL_POLL = "CHANNEL_POLL"; public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS"; + public static final String CHANNEL_SIGN_UP = "CHANNEL_SIGN_UP"; /** * WorkManager Tag @@ -392,6 +393,7 @@ public class NotificationHelper { CHANNEL_FAVOURITE + account.getIdentifier(), CHANNEL_POLL + account.getIdentifier(), CHANNEL_SUBSCRIPTIONS + account.getIdentifier(), + CHANNEL_SIGN_UP + account.getIdentifier(), }; int[] channelNames = { R.string.notification_mention_name, @@ -401,6 +403,7 @@ public class NotificationHelper { R.string.notification_favourite_name, R.string.notification_poll_name, R.string.notification_subscription_name, + R.string.notification_sign_up_name, }; int[] channelDescriptions = { R.string.notification_mention_descriptions, @@ -410,6 +413,7 @@ public class NotificationHelper { R.string.notification_favourite_description, R.string.notification_poll_description, R.string.notification_subscription_description, + R.string.notification_sign_up_description, }; List channels = new ArrayList<>(6); @@ -560,6 +564,8 @@ public class NotificationHelper { return account.getNotificationsFavorited(); case POLL: return account.getNotificationsPolls(); + case SIGN_UP: + return account.getNotificationsSignUps(); default: return false; } @@ -582,6 +588,8 @@ public class NotificationHelper { return CHANNEL_FAVOURITE + account.getIdentifier(); case POLL: return CHANNEL_POLL + account.getIdentifier(); + case SIGN_UP: + return CHANNEL_SIGN_UP + account.getIdentifier(); default: return null; } @@ -663,6 +671,8 @@ public class NotificationHelper { } else { return context.getString(R.string.poll_ended_voted); } + case SIGN_UP: + return String.format(context.getString(R.string.notification_sign_up_format), accountName); } return null; } @@ -671,6 +681,7 @@ public class NotificationHelper { switch (notification.getType()) { case FOLLOW: case FOLLOW_REQUEST: + case SIGN_UP: return "@" + notification.getAccount().getUsername(); case MENTION: case FAVOURITE: diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt index 4d8ba84f..82ee0a38 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt @@ -122,6 +122,17 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable { true } } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_sign_ups) + key = PrefKeys.NOTIFICATION_FILTER_SIGN_UPS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsSignUps + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsSignUps = newValue as Boolean } + true + } + } } preferenceCategory(R.string.pref_title_notification_alerts) { category -> diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt index 0c25cbbc..5da91e20 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt @@ -50,6 +50,7 @@ data class AccountEntity( var notificationsFavorited: Boolean = true, var notificationsPolls: Boolean = true, var notificationsSubscriptions: Boolean = true, + var notificationsSignUps: Boolean = true, var notificationSound: Boolean = true, var notificationVibration: Boolean = true, var notificationLight: Boolean = true, diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 159a6f52..2131300c 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 = 31) + }, version = 32) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -483,4 +483,11 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("DELETE FROM `TimelineStatusEntity`"); } }; + + public static final Migration MIGRATION_31_32 = new Migration(31, 32) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSignUps` INTEGER NOT NULL DEFAULT 1"); + } + }; } 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 b0f28261..677f8167 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -62,7 +62,7 @@ class AppModule { AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25, AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")), AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29, - AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31 + AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32, ) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt index ae2d74a9..ddcf5e61 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt @@ -37,7 +37,9 @@ data class Notification( FOLLOW("follow"), FOLLOW_REQUEST("follow_request"), POLL("poll"), - STATUS("status"); + STATUS("status"), + SIGN_UP("admin.sign_up"), + ; companion object { @@ -49,7 +51,7 @@ data class Notification( } return UNKNOWN } - val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS) + val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS, SIGN_UP) } override fun toString(): String { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 467ebc8c..f267f29e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -15,6 +15,10 @@ package com.keylesspalace.tusky.fragment; +import static com.keylesspalace.tusky.util.StringUtils.isLessThan; +import static autodispose2.AutoDispose.autoDisposable; +import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from; + import android.app.Activity; import android.content.Context; import android.content.DialogInterface; @@ -111,10 +115,6 @@ import kotlin.Unit; import kotlin.collections.CollectionsKt; import kotlin.jvm.functions.Function1; -import static autodispose2.AutoDispose.autoDisposable; -import static autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from; -import static com.keylesspalace.tusky.util.StringUtils.isLessThan; - public class NotificationsFragment extends SFragment implements SwipeRefreshLayout.OnRefreshListener, StatusActionListener, @@ -707,6 +707,8 @@ public class NotificationsFragment extends SFragment implements return getString(R.string.notification_poll_name); case STATUS: return getString(R.string.notification_subscription_name); + case SIGN_UP: + return getString(R.string.notification_sign_up_name); default: return "Unknown"; } diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt index c59ba58b..c728e1f6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt @@ -59,6 +59,7 @@ object PrefKeys { const val NOTIFICATION_FILTER_FOLLOW_REQUESTS = "notificationFilterFollowRequests" const val NOTIFICATIONS_FILTER_FOLLOWS = "notificationFilterFollows" const val NOTIFICATION_FILTER_SUBSCRIPTIONS = "notificationFilterSubscriptions" + const val NOTIFICATION_FILTER_SIGN_UPS = "notificationFilterSignUps" const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies" const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e9daafe0..e6ce2338 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -62,6 +62,7 @@ %s favorited your post %s followed you %s requested to follow you + %s signed up %s just posted Report @%s @@ -228,6 +229,7 @@ my posts are favorited polls have ended somebody I\'m subscribed to published a new post + somebody signed up Appearance App Theme Timelines @@ -295,6 +297,8 @@ Notifications about polls that have ended New posts Notifications when somebody you\'re subscribed to published a new post + Sign ups + Notifications about new users %s mentioned you %1$s, %2$s, %3$s and %4$d others From 3e8c6a318a99744e4f7c480fc43c43130d1d1c75 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 14 Apr 2022 19:49:49 +0200 Subject: [PATCH 002/104] introduce KotlinResultCallAdapter for nice suspending network calls (#2415) * introduce KotlinResultCallAdapter for nice suspending network calls * fix tests --- app/build.gradle | 5 +- .../com/keylesspalace/tusky/MainActivity.kt | 21 ++-- .../announcements/AnnouncementsViewModel.kt | 6 +- .../components/compose/ComposeViewModel.kt | 8 +- .../tusky/components/login/LoginActivity.kt | 95 +++++++++---------- .../scheduled/ScheduledStatusViewModel.kt | 15 +-- .../keylesspalace/tusky/di/NetworkModule.kt | 2 + .../tusky/network/MastodonApi.kt | 16 ++-- .../tusky/viewmodel/EditProfileViewModel.kt | 87 ++++++++--------- .../tusky/BottomSheetActivityTest.kt | 10 +- .../tusky/ComposeActivityTest.kt | 57 ++++++----- .../com/keylesspalace/tusky/FilterTest.kt | 4 +- .../CachedTimelineRemoteMediatorTest.kt | 6 +- .../NetworkTimelinePagingSourceTest.kt | 4 +- .../NetworkTimelineRemoteMediatorTest.kt | 11 +-- 15 files changed, 168 insertions(+), 179 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 645bcb05..f16f9675 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -137,6 +137,7 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion" + implementation "at.connyduck:kotlin-result-calladapter:1.0.0" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" @@ -176,8 +177,8 @@ dependencies { testImplementation "androidx.test.ext:junit:1.1.3" testImplementation "org.robolectric:robolectric:4.4" - testImplementation "org.mockito:mockito-inline:3.6.28" - testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" + testImplementation "org.mockito:mockito-inline:4.4.0" + testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0" androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" androidTestImplementation "androidx.room:room-testing:$roomVersion" diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 5c7901c5..a934ff9a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -682,18 +682,15 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } } - private fun fetchUserInfo() { - mastodonApi.accountVerifyCredentials() - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe( - { userInfo -> - onFetchUserInfoSuccess(userInfo) - }, - { throwable -> - Log.e(TAG, "Failed to fetch user info. " + throwable.message) - } - ) + private fun fetchUserInfo() = lifecycleScope.launch { + mastodonApi.accountVerifyCredentials().fold( + { userInfo -> + onFetchUserInfoSuccess(userInfo) + }, + { throwable -> + Log.e(TAG, "Failed to fetch user info. " + throwable.message) + } + ) } private fun onFetchUserInfoSuccess(me: Account) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt index d1ae0b9e..10dc303f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt @@ -35,6 +35,7 @@ import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.RxAwareViewModel import com.keylesspalace.tusky.util.Success import io.reactivex.rxjava3.core.Single +import kotlinx.coroutines.rx3.rxSingle import javax.inject.Inject class AnnouncementsViewModel @Inject constructor( @@ -56,8 +57,9 @@ class AnnouncementsViewModel @Inject constructor( appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) .map> { Either.Left(it) } .onErrorResumeNext { - mastodonApi.getInstance() - .map { Either.Right(it) } + rxSingle { + mastodonApi.getInstance().getOrThrow() + }.map { Either.Right(it) } } ) { emojis, either -> either.asLeftOrNull()?.copy(emojiList = emojis) 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 66dacfb4..08df6dc9 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 @@ -48,6 +48,7 @@ import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.Disposable import kotlinx.coroutines.launch +import kotlinx.coroutines.rx3.rxSingle import java.util.Locale import javax.inject.Inject @@ -105,7 +106,10 @@ class ComposeViewModel @Inject constructor( init { Single.zip( - api.getCustomEmojis(), api.getInstance() + api.getCustomEmojis(), + rxSingle { + api.getInstance().getOrThrow() + } ) { emojis, instance -> InstanceEntity( instance = accountManager.activeAccount?.domain!!, @@ -291,7 +295,7 @@ class ComposeViewModel @Inject constructor( ): LiveData { val deletionObservable = if (isEditingScheduledToot) { - api.deleteScheduledStatus(scheduledTootId.toString()).toObservable().map { } + rxSingle { api.deleteScheduledStatus(scheduledTootId.toString()) }.toObservable().map { } } else { Observable.just(Unit) }.toLiveData() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt index cc2bd776..4df7abc1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt @@ -33,7 +33,6 @@ import com.keylesspalace.tusky.MainActivity import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.ActivityLoginBinding import com.keylesspalace.tusky.di.Injectable -import com.keylesspalace.tusky.entity.AppCredentials import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.getNonNullString import com.keylesspalace.tusky.util.rickRoll @@ -166,32 +165,33 @@ class LoginActivity : BaseActivity(), Injectable { setLoading(true) lifecycleScope.launch { - val credentials: AppCredentials = try { - mastodonApi.authenticateApp( - domain, getString(R.string.app_name), oauthRedirectUri, - OAUTH_SCOPES, getString(R.string.tusky_website) - ) - } catch (e: Exception) { - binding.loginButton.isEnabled = true - binding.domainTextInputLayout.error = - getString(R.string.error_failed_app_registration) - setLoading(false) - Log.e(TAG, Log.getStackTraceString(e)) - return@launch - } + mastodonApi.authenticateApp( + domain, getString(R.string.app_name), oauthRedirectUri, + OAUTH_SCOPES, getString(R.string.tusky_website) + ).fold( + { credentials -> + // Before we open browser page we save the data. + // Even if we don't open other apps user may go to password manager or somewhere else + // and we will need to pick up the process where we left off. + // Alternatively we could pass it all as part of the intent and receive it back + // but it is a bit of a workaround. + preferences.edit() + .putString(DOMAIN, domain) + .putString(CLIENT_ID, credentials.clientId) + .putString(CLIENT_SECRET, credentials.clientSecret) + .apply() - // Before we open browser page we save the data. - // Even if we don't open other apps user may go to password manager or somewhere else - // and we will need to pick up the process where we left off. - // Alternatively we could pass it all as part of the intent and receive it back - // but it is a bit of a workaround. - preferences.edit() - .putString(DOMAIN, domain) - .putString(CLIENT_ID, credentials.clientId) - .putString(CLIENT_SECRET, credentials.clientSecret) - .apply() - - redirectUserToAuthorizeAndLogin(domain, credentials.clientId) + redirectUserToAuthorizeAndLogin(domain, credentials.clientId) + }, + { e -> + binding.loginButton.isEnabled = true + binding.domainTextInputLayout.error = + getString(R.string.error_failed_app_registration) + setLoading(false) + Log.e(TAG, Log.getStackTraceString(e)) + return@launch + } + ) } } @@ -224,29 +224,28 @@ class LoginActivity : BaseActivity(), Injectable { setLoading(true) - val accessToken = try { - mastodonApi.fetchOAuthToken( - domain, clientId, clientSecret, oauthRedirectUri, code, - "authorization_code" - ) - } catch (e: Exception) { - setLoading(false) - binding.domainTextInputLayout.error = - getString(R.string.error_retrieving_oauth_token) - Log.e( - TAG, - "%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message), - ) - return - } + mastodonApi.fetchOAuthToken( + domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code" + ).fold( + { accessToken -> + accountManager.addAccount(accessToken.accessToken, domain) - accountManager.addAccount(accessToken.accessToken, domain) - - val intent = Intent(this, MainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - startActivity(intent) - finish() - overridePendingTransition(R.anim.explode, R.anim.explode) + val intent = Intent(this, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + finish() + overridePendingTransition(R.anim.explode, R.anim.explode) + }, + { e -> + setLoading(false) + binding.domainTextInputLayout.error = + getString(R.string.error_retrieving_oauth_token) + Log.e( + TAG, + "%s %s".format(getString(R.string.error_retrieving_oauth_token), e.message), + ) + } + ) } private fun setLoading(loadingState: Boolean) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt index cd3e5ac0..766ed44a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt @@ -25,7 +25,6 @@ import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.network.MastodonApi import kotlinx.coroutines.launch -import kotlinx.coroutines.rx3.await import javax.inject.Inject class ScheduledStatusViewModel @Inject constructor( @@ -43,12 +42,14 @@ class ScheduledStatusViewModel @Inject constructor( fun deleteScheduledStatus(status: ScheduledStatus) { viewModelScope.launch { - try { - mastodonApi.deleteScheduledStatus(status.id).await() - pagingSourceFactory.remove(status) - } catch (throwable: Throwable) { - Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable) - } + mastodonApi.deleteScheduledStatus(status.id).fold( + { + pagingSourceFactory.remove(status) + }, + { throwable -> + Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable) + } + ) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index 7bda6ef7..d927c299 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -19,6 +19,7 @@ import android.content.Context import android.content.SharedPreferences import android.os.Build import android.text.Spanned +import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory import com.google.gson.Gson import com.google.gson.GsonBuilder import com.keylesspalace.tusky.BuildConfig @@ -111,6 +112,7 @@ class NetworkModule { .client(httpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) + .addCallAdapterFactory(KotlinResultCallAdapterFactory.create()) .build() } 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 28d83eca..111cad56 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -80,7 +80,7 @@ interface MastodonApi { fun getCustomEmojis(): Single> @GET("api/v1/instance") - fun getInstance(): Single + suspend fun getInstance(): Result @GET("api/v1/filters") fun getFilters(): Single> @@ -249,12 +249,12 @@ interface MastodonApi { ): Single> @DELETE("api/v1/scheduled_statuses/{id}") - fun deleteScheduledStatus( + suspend fun deleteScheduledStatus( @Path("id") scheduledStatusId: String - ): Single + ): Result @GET("api/v1/accounts/verify_credentials") - fun accountVerifyCredentials(): Single + suspend fun accountVerifyCredentials(): Result @FormUrlEncoded @PATCH("api/v1/accounts/update_credentials") @@ -265,7 +265,7 @@ interface MastodonApi { @Multipart @PATCH("api/v1/accounts/update_credentials") - fun accountUpdateCredentials( + suspend fun accountUpdateCredentials( @Part(value = "display_name") displayName: RequestBody?, @Part(value = "note") note: RequestBody?, @Part(value = "locked") locked: RequestBody?, @@ -279,7 +279,7 @@ interface MastodonApi { @Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?, @Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?, @Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody? - ): Call + ): Result @GET("api/v1/accounts/search") fun searchAccounts( @@ -447,7 +447,7 @@ interface MastodonApi { @Field("redirect_uris") redirectUris: String, @Field("scopes") scopes: String, @Field("website") website: String - ): AppCredentials + ): Result @FormUrlEncoded @POST("oauth/token") @@ -458,7 +458,7 @@ interface MastodonApi { @Field("redirect_uri") redirectUri: String, @Field("code") code: String, @Field("grant_type") grantType: String - ): AccessToken + ): Result @FormUrlEncoded @POST("api/v1/lists") 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 f3539f8d..17aa38c7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -20,6 +20,7 @@ import android.net.Uri import androidx.core.net.toUri import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.ProfileEditedEvent import com.keylesspalace.tusky.entity.Account @@ -31,8 +32,7 @@ import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.randomAlphanumericString -import io.reactivex.rxjava3.disposables.CompositeDisposable -import io.reactivex.rxjava3.kotlin.addTo +import kotlinx.coroutines.launch import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import okhttp3.RequestBody @@ -40,9 +40,7 @@ import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody import org.json.JSONException import org.json.JSONObject -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import retrofit2.HttpException import java.io.File import javax.inject.Inject @@ -63,24 +61,20 @@ class EditProfileViewModel @Inject constructor( private var oldProfileData: Account? = null - private val disposables = CompositeDisposable() - - fun obtainProfile() { + fun obtainProfile() = viewModelScope.launch { if (profileData.value == null || profileData.value is Error) { profileData.postValue(Loading()) - mastodonApi.accountVerifyCredentials() - .subscribe( - { profile -> - oldProfileData = profile - profileData.postValue(Success(profile)) - }, - { - profileData.postValue(Error()) - } - ) - .addTo(disposables) + mastodonApi.accountVerifyCredentials().fold( + { profile -> + oldProfileData = profile + profileData.postValue(Success(profile)) + }, + { + profileData.postValue(Error()) + } + ) } } @@ -151,34 +145,34 @@ class EditProfileViewModel @Inject constructor( return } - mastodonApi.accountUpdateCredentials( - displayName, note, locked, avatar, header, - field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second - ).enqueue(object : Callback { - override fun onResponse(call: Call, response: Response) { - val newProfileData = response.body() - if (!response.isSuccessful || newProfileData == null) { - val errorResponse = response.errorBody()?.string() - val errorMsg = if (!errorResponse.isNullOrBlank()) { - try { - JSONObject(errorResponse).optString("error", null) - } catch (e: JSONException) { + viewModelScope.launch { + mastodonApi.accountUpdateCredentials( + displayName, note, locked, avatar, header, + field1?.first, field1?.second, field2?.first, field2?.second, field3?.first, field3?.second, field4?.first, field4?.second + ).fold( + { newProfileData -> + saveData.postValue(Success()) + eventHub.dispatch(ProfileEditedEvent(newProfileData)) + }, + { throwable -> + if (throwable is HttpException) { + val errorResponse = throwable.response()?.errorBody()?.string() + val errorMsg = if (!errorResponse.isNullOrBlank()) { + try { + JSONObject(errorResponse).optString("error", "") + } catch (e: JSONException) { + null + } + } else { null } + saveData.postValue(Error(errorMessage = errorMsg)) } else { - null + saveData.postValue(Error()) } - saveData.postValue(Error(errorMessage = errorMsg)) - return } - saveData.postValue(Success()) - eventHub.dispatch(ProfileEditedEvent(newProfileData)) - } - - override fun onFailure(call: Call, t: Throwable) { - saveData.postValue(Error()) - } - }) + ) + } } // cache activity state for rotation change @@ -208,15 +202,11 @@ class EditProfileViewModel @Inject constructor( return File(application.cacheDir, filename) } - override fun onCleared() { - disposables.dispose() - } - - fun obtainInstance() { + fun obtainInstance() = viewModelScope.launch { if (instanceData.value == null || instanceData.value is Error) { instanceData.postValue(Loading()) - mastodonApi.getInstance().subscribe( + mastodonApi.getInstance().fold( { instance -> instanceData.postValue(Success(instance)) }, @@ -224,7 +214,6 @@ class EditProfileViewModel @Inject constructor( instanceData.postValue(Error()) } ) - .addTo(disposables) } } } diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt index ef6d2632..ff208823 100644 --- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt @@ -16,15 +16,11 @@ package com.keylesspalace.tusky import android.text.SpannedString -import android.widget.LinearLayout import androidx.arch.core.executor.testing.InstantTaskExecutorRule -import com.google.android.material.bottomsheet.BottomSheetBehavior import com.keylesspalace.tusky.entity.SearchResult import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.network.MastodonApi -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.mock import io.reactivex.rxjava3.android.plugins.RxAndroidPlugins import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.plugins.RxJavaPlugins @@ -39,8 +35,8 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.Mockito.eq -import org.mockito.Mockito.mock -import java.util.ArrayList +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock import java.util.Date import java.util.concurrent.TimeUnit @@ -306,7 +302,7 @@ class BottomSheetActivityTest { init { mastodonApi = api @Suppress("UNCHECKED_CAST") - bottomSheet = mock(BottomSheetBehavior::class.java) as BottomSheetBehavior + bottomSheet = mock() } override fun openLink(url: String) { diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index e7b3a1a9..dc4a412f 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -24,8 +24,6 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH -import com.keylesspalace.tusky.components.compose.MediaUploader -import com.keylesspalace.tusky.components.drafts.DraftHelper import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase @@ -37,18 +35,16 @@ import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.InstanceConfiguration import com.keylesspalace.tusky.entity.StatusConfiguration import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.service.ServiceClient -import com.nhaarman.mockitokotlin2.any import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.core.SingleObserver import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.`when` -import org.mockito.Mockito.mock +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock import org.robolectric.Robolectric import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config @@ -94,44 +90,47 @@ class ComposeActivityTest { val controller = Robolectric.buildActivity(ComposeActivity::class.java) activity = controller.get() - accountManagerMock = mock(AccountManager::class.java) - `when`(accountManagerMock.activeAccount).thenReturn(account) + accountManagerMock = mock { + on { activeAccount } doReturn account + } - apiMock = mock(MastodonApi::class.java) - `when`(apiMock.getCustomEmojis()).thenReturn(Single.just(emptyList())) - `when`(apiMock.getInstance()).thenReturn(object : Single() { - override fun subscribeActual(observer: SingleObserver) { - val instance = instanceResponseCallback?.invoke() + apiMock = mock { + on { getCustomEmojis() } doReturn Single.just(emptyList()) + onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance -> if (instance == null) { - observer.onError(Throwable()) + Result.failure(Throwable()) } else { - observer.onSuccess(instance) + Result.success(instance) } } - }) + } - val instanceDaoMock = mock(InstanceDao::class.java) - `when`(instanceDaoMock.loadMetadataForInstance(any())).thenReturn( - Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null)) - ) + val instanceDaoMock: InstanceDao = mock { + on { loadMetadataForInstance(any()) } doReturn + Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null)) + on { loadMetadataForInstance(any()) } doReturn + Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null)) + } - val dbMock = mock(AppDatabase::class.java) - `when`(dbMock.instanceDao()).thenReturn(instanceDaoMock) + val dbMock: AppDatabase = mock { + on { instanceDao() } doReturn instanceDaoMock + } val viewModel = ComposeViewModel( apiMock, accountManagerMock, - mock(MediaUploader::class.java), - mock(ServiceClient::class.java), - mock(DraftHelper::class.java), + mock(), + mock(), + mock(), dbMock ) activity.intent = Intent(activity, ComposeActivity::class.java).apply { putExtra(ComposeActivity.COMPOSE_OPTIONS_EXTRA, composeOptions) } - val viewModelFactoryMock = mock(ViewModelFactory::class.java) - `when`(viewModelFactoryMock.create(ComposeViewModel::class.java)).thenReturn(viewModel) + val viewModelFactoryMock: ViewModelFactory = mock { + on { create(ComposeViewModel::class.java) } doReturn viewModel + } activity.accountManager = accountManagerMock activity.viewModelFactory = viewModelFactoryMock @@ -490,7 +489,7 @@ class ComposeActivityTest { ) } - fun getCustomInstanceConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): InstanceConfiguration { + private fun getCustomInstanceConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): InstanceConfiguration { return InstanceConfiguration( statuses = StatusConfiguration( maxCharacters = maximumStatusCharacters, diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt index 03fff5ee..d5063943 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt @@ -8,12 +8,12 @@ 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.nhaarman.mockitokotlin2.mock import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock import org.robolectric.annotation.Config import java.util.ArrayList import java.util.Date @@ -22,7 +22,7 @@ import java.util.Date @RunWith(AndroidJUnit4::class) class FilterTest { - lateinit var filterModel: FilterModel + private lateinit var filterModel: FilterModel @Before fun setup() { 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 462b0a4a..2778f8c2 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,9 +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 com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.mock import io.reactivex.rxjava3.core.Single import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.runBlocking @@ -31,6 +28,9 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config import retrofit2.HttpException diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt index 2e67c6fe..60dda419 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt @@ -3,11 +3,11 @@ package com.keylesspalace.tusky.components.timeline import androidx.paging.PagingSource import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelinePagingSource import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.mock import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock class NetworkTimelinePagingSourceTest { diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt index 74d0fe25..eabf744c 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt @@ -12,11 +12,6 @@ import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineView import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.viewdata.StatusViewData -import com.nhaarman.mockitokotlin2.anyOrNull -import com.nhaarman.mockitokotlin2.doReturn -import com.nhaarman.mockitokotlin2.doThrow -import com.nhaarman.mockitokotlin2.mock -import com.nhaarman.mockitokotlin2.verify import kotlinx.coroutines.runBlocking import okhttp3.Headers import okhttp3.ResponseBody.Companion.toResponseBody @@ -24,6 +19,11 @@ import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue 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.mockito.kotlin.verify import org.robolectric.annotation.Config import retrofit2.HttpException import retrofit2.Response @@ -331,7 +331,6 @@ class NetworkTimelineRemoteMediatorTest { mockStatusViewData("2"), mockStatusViewData("1"), ) - verify(timelineViewModel).nextKey = "0" assertTrue(result is RemoteMediator.MediatorResult.Success) assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) From ad077cf09293ded4db4e9a00e73b6d04a9a20b01 Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Thu, 14 Apr 2022 19:58:08 +0200 Subject: [PATCH 003/104] Don't show preview cards on statuses with polls. (#2430) Fixes #2427 --- .../keylesspalace/tusky/adapter/StatusBaseViewHolder.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) 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 1239ea71..dbca518a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -1043,9 +1043,11 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { StatusDisplayOptions statusDisplayOptions, final StatusActionListener listener ) { - final Card card = status.getActionable().getCard(); + final Status actionable = status.getActionable(); + final Card card = actionable.getCard(); if (cardViewMode != CardViewMode.NONE && - status.getActionable().getAttachments().size() == 0 && + actionable.getAttachments().size() == 0 && + actionable.getPoll() == null && card != null && !TextUtils.isEmpty(card.getUrl()) && (!status.isCollapsible() || !status.isCollapsed())) { @@ -1067,7 +1069,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { // Statuses from other activitypub sources can be marked sensitive even if there's no media, // so let's blur the preview in that case // If media previews are disabled, show placeholder for cards as well - if (statusDisplayOptions.mediaPreviewEnabled() && !status.getActionable().getSensitive() && !TextUtils.isEmpty(card.getImage())) { + if (statusDisplayOptions.mediaPreviewEnabled() && !actionable.getSensitive() && !TextUtils.isEmpty(card.getImage())) { int topLeftRadius = 0; int topRightRadius = 0; From 7aa328b3dced477946ef00dee9e4eef38da18905 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 15 Apr 2022 10:50:28 +0200 Subject: [PATCH 004/104] fix login on Android API level <24 (#2432) --- .../components/login/LoginWebViewActivity.kt | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt index 01f6c3b0..827b5620 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt @@ -16,6 +16,7 @@ import android.webkit.WebStorage import android.webkit.WebView import android.webkit.WebViewClient import androidx.activity.result.contract.ActivityResultContract +import androidx.core.net.toUri import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.databinding.LoginWebviewBinding @@ -103,8 +104,8 @@ class LoginWebViewActivity : BaseActivity(), Injectable { webView.webViewClient = object : WebViewClient() { override fun onReceivedError( - view: WebView?, - request: WebResourceRequest?, + view: WebView, + request: WebResourceRequest, error: WebResourceError ) { Log.d("LoginWeb", "Failed to load ${data.url}: $error") @@ -115,7 +116,17 @@ class LoginWebViewActivity : BaseActivity(), Injectable { view: WebView, request: WebResourceRequest ): Boolean { - val url = request.url + return shouldOverrideUrlLoading(request.url) + } + + /* overriding this deprecated method is necessary for it to work on api levels < 24 */ + @Suppress("OVERRIDE_DEPRECATION") + override fun shouldOverrideUrlLoading(view: WebView?, urlString: String?): Boolean { + val url = urlString?.toUri() ?: return false + return shouldOverrideUrlLoading(url) + } + + fun shouldOverrideUrlLoading(url: Uri): Boolean { return if (url.scheme == oauthUrl.scheme && url.host == oauthUrl.host) { val error = url.getQueryParameter("error") if (error != null) { @@ -130,6 +141,7 @@ class LoginWebViewActivity : BaseActivity(), Injectable { } } } + webView.setBackgroundColor(Color.TRANSPARENT) if (savedInstanceState == null) { From ffbc4b64037901a6d257cfbdc6bd09cbfc64ba88 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 15 Apr 2022 11:00:36 +0200 Subject: [PATCH 005/104] upgrade Kotlin Result CallAdapter to v1.0.1 to fix crash (#2433) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index f16f9675..3c5cb162 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -137,7 +137,7 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:$retrofitVersion" implementation "com.squareup.retrofit2:converter-gson:$retrofitVersion" implementation "com.squareup.retrofit2:adapter-rxjava3:$retrofitVersion" - implementation "at.connyduck:kotlin-result-calladapter:1.0.0" + implementation "at.connyduck:kotlin-result-calladapter:1.0.1" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" From 3e849244f9d1c77eee4614193832f9b263961ab0 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 15 Apr 2022 13:20:27 +0200 Subject: [PATCH 006/104] move Html parsing to ViewData (#2414) * move Html parsing to ViewData * refactor reports to use viewdata * cleanup code * refactor conversations * fix getEditableText * rename StatusParsingHelper * fix tests * commit db schema file * add file header * rename helper function to parseAsMastodonHtml * order imports correctly * move mapping off main thread to default dispatcher * fix ktlint --- .../33.json | 809 ++++++++++++++++++ .../components/account/AccountActivity.kt | 24 +- .../components/account/AccountFieldAdapter.kt | 3 +- .../conversation/ConversationAdapter.kt | 12 +- .../conversation/ConversationEntity.kt | 133 +-- .../conversation/ConversationViewData.kt | 87 ++ .../conversation/ConversationViewHolder.java | 20 +- .../conversation/ConversationsFragment.kt | 30 +- .../conversation/ConversationsViewModel.kt | 79 +- .../components/report/ReportViewModel.kt | 12 +- .../report/adapter/StatusViewHolder.kt | 58 +- .../report/adapter/StatusesAdapter.kt | 12 +- .../timeline/TimelineTypeMappers.kt | 16 +- .../viewmodel/CachedTimelineViewModel.kt | 11 +- .../viewmodel/NetworkTimelineViewModel.kt | 6 +- .../keylesspalace/tusky/db/AppDatabase.java | 39 +- .../tusky/db/ConversationsDao.kt | 5 +- .../com/keylesspalace/tusky/db/Converters.kt | 21 - .../com/keylesspalace/tusky/di/AppModule.kt | 1 + .../keylesspalace/tusky/di/NetworkModule.kt | 9 +- .../com/keylesspalace/tusky/entity/Account.kt | 55 +- .../tusky/entity/Announcement.kt | 3 +- .../com/keylesspalace/tusky/entity/Card.kt | 9 +- .../com/keylesspalace/tusky/entity/Status.kt | 74 +- .../tusky/json/SpannedTypeAdapter.kt | 54 -- .../tusky/util/StatusParsingHelper.kt | 62 ++ .../keylesspalace/tusky/util/ViewDataUtils.kt | 3 - .../tusky/viewdata/StatusViewData.kt | 53 +- .../tusky/BottomSheetActivityTest.kt | 3 +- .../tusky/ComposeActivityTest.kt | 3 +- .../com/keylesspalace/tusky/FilterTest.kt | 3 +- .../tusky/StatusComparisonTest.kt | 14 +- .../NetworkTimelinePagingSourceTest.kt | 5 + .../tusky/components/timeline/StatusMocker.kt | 4 +- 34 files changed, 1232 insertions(+), 500 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json new file mode 100644 index 00000000..e6d8ec7d --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json @@ -0,0 +1,809 @@ +{ + "formatVersion": 1, + "database": { + "version": 33, + "identityHash": "920a0e0c9a600bd236f6bf959b469c18", + "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, `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, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "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, 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 + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT 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, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": 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 + } + ], + "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, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_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": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.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, '920a0e0c9a600bd236f6bf959b469c18')" + ] + } +} \ No newline at end of file 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 c218e111..114a6cd0 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 @@ -78,7 +78,7 @@ import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.getDomain import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.loadAvatar -import com.keylesspalace.tusky.util.openLink +import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.setClickableText import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding @@ -375,12 +375,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } } viewModel.accountFieldData.observe( - this, - { - accountFieldAdapter.fields = it - accountFieldAdapter.notifyDataSetChanged() - } - ) + this + ) { + accountFieldAdapter.fields = it + accountFieldAdapter.notifyDataSetChanged() + } viewModel.noteSaved.observe(this) { binding.saveNoteInfo.visible(it, View.INVISIBLE) } @@ -395,11 +394,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI adapter.refreshContent() } viewModel.isRefreshing.observe( - this, - { isRefreshing -> - binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true - } - ) + this + ) { isRefreshing -> + binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true + } binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue) } @@ -410,7 +408,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI binding.accountUsernameTextView.text = usernameFormatted binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis) - val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis) + val emojifiedNote = account.note.parseAsMastodonHtml().emojify(account.emojis, binding.accountNoteTextView, animateEmojis) setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this) // accountFieldAdapter.fields = account.fields ?: emptyList() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt index 093dbcfb..d51bb145 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt @@ -29,6 +29,7 @@ import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.createClickableText import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.setClickableText class AccountFieldAdapter( @@ -65,7 +66,7 @@ class AccountFieldAdapter( val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis) nameTextView.text = emojifiedName - val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis) + val emojifiedValue = field.value.parseAsMastodonHtml().emojify(emojis, valueTextView, animateEmojis) setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener) if (field.verifiedAt != null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt index 89c1ad0f..0c946514 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt @@ -26,7 +26,7 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions class ConversationAdapter( private val statusDisplayOptions: StatusDisplayOptions, private val listener: StatusActionListener -) : PagingDataAdapter(CONVERSATION_COMPARATOR) { +) : PagingDataAdapter(CONVERSATION_COMPARATOR) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false) @@ -37,17 +37,13 @@ class ConversationAdapter( holder.setupWithConversation(getItem(position)) } - fun item(position: Int): ConversationEntity? { - return getItem(position) - } - companion object { - val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean { + val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean { return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean { + override fun areContentsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean { return oldItem == newItem } } 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 88c9dbad..f585b4ea 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 @@ -15,7 +15,6 @@ package com.keylesspalace.tusky.components.conversation -import android.text.Spanned import androidx.room.Embedded import androidx.room.Entity import androidx.room.TypeConverters @@ -27,7 +26,7 @@ import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.TimelineAccount -import com.keylesspalace.tusky.util.shouldTrimStatus +import com.keylesspalace.tusky.viewdata.StatusViewData import java.util.Date @Entity(primaryKeys = ["id", "accountId"]) @@ -38,7 +37,16 @@ data class ConversationEntity( val accounts: List, val unread: Boolean, @Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity -) +) { + fun toViewData(): ConversationViewData { + return ConversationViewData( + id = id, + accounts = accounts, + unread = unread, + lastStatus = lastStatus.toViewData() + ) + } +} data class ConversationAccountEntity( val id: String, @@ -67,7 +75,7 @@ data class ConversationStatusEntity( val inReplyToId: String?, val inReplyToAccountId: String?, val account: ConversationAccountEntity, - val content: Spanned, + val content: String, val createdAt: Date, val emojis: List, val favouritesCount: Int, @@ -80,95 +88,43 @@ data class ConversationStatusEntity( val tags: List?, val showingHiddenContent: Boolean, val expanded: Boolean, - val collapsible: Boolean, val collapsed: Boolean, val muted: Boolean, val poll: Poll? ) { - /** its necessary to override this because Spanned.equals does not work as expected */ - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as ConversationStatusEntity - - if (id != other.id) return false - if (url != other.url) return false - if (inReplyToId != other.inReplyToId) return false - if (inReplyToAccountId != other.inReplyToAccountId) return false - if (account != other.account) return false - if (content.toString() != other.content.toString()) return false - if (createdAt != other.createdAt) return false - if (emojis != other.emojis) return false - if (favouritesCount != other.favouritesCount) return false - if (favourited != other.favourited) return false - if (sensitive != other.sensitive) return false - if (spoilerText != other.spoilerText) return false - if (attachments != other.attachments) return false - if (mentions != other.mentions) return false - if (tags != other.tags) return false - if (showingHiddenContent != other.showingHiddenContent) return false - if (expanded != other.expanded) return false - if (collapsible != other.collapsible) return false - if (collapsed != other.collapsed) return false - if (muted != other.muted) return false - if (poll != other.poll) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + (url?.hashCode() ?: 0) - result = 31 * result + (inReplyToId?.hashCode() ?: 0) - result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0) - result = 31 * result + account.hashCode() - result = 31 * result + content.toString().hashCode() - result = 31 * result + createdAt.hashCode() - result = 31 * result + emojis.hashCode() - result = 31 * result + favouritesCount - result = 31 * result + favourited.hashCode() - result = 31 * result + sensitive.hashCode() - result = 31 * result + spoilerText.hashCode() - result = 31 * result + attachments.hashCode() - result = 31 * result + mentions.hashCode() - result = 31 * result + tags.hashCode() - result = 31 * result + showingHiddenContent.hashCode() - result = 31 * result + expanded.hashCode() - result = 31 * result + collapsible.hashCode() - result = 31 * result + collapsed.hashCode() - result = 31 * result + muted.hashCode() - result = 31 * result + poll.hashCode() - return result - } - - fun toStatus(): Status { - return Status( - id = id, - url = url, - account = account.toAccount(), - inReplyToId = inReplyToId, - inReplyToAccountId = inReplyToAccountId, - content = content, - reblog = null, - createdAt = createdAt, - emojis = emojis, - reblogsCount = 0, - favouritesCount = favouritesCount, - reblogged = false, - favourited = favourited, - bookmarked = bookmarked, - sensitive = sensitive, - spoilerText = spoilerText, - visibility = Status.Visibility.DIRECT, - attachments = attachments, - mentions = mentions, - tags = tags, - application = null, - pinned = false, - muted = muted, - poll = poll, - card = null + fun toViewData(): StatusViewData.Concrete { + return StatusViewData.Concrete( + status = Status( + id = id, + url = url, + account = account.toAccount(), + inReplyToId = inReplyToId, + inReplyToAccountId = inReplyToAccountId, + content = content, + reblog = null, + createdAt = createdAt, + emojis = emojis, + reblogsCount = 0, + favouritesCount = favouritesCount, + reblogged = false, + favourited = favourited, + bookmarked = bookmarked, + sensitive = sensitive, + spoilerText = spoilerText, + visibility = Status.Visibility.DIRECT, + attachments = attachments, + mentions = mentions, + tags = tags, + application = null, + pinned = false, + muted = muted, + poll = poll, + card = null + ), + isExpanded = expanded, + isShowingContent = showingHiddenContent, + isCollapsed = collapsed ) } } @@ -202,7 +158,6 @@ fun Status.toEntity() = tags = tags, showingHiddenContent = false, expanded = false, - collapsible = shouldTrimStatus(content), collapsed = true, muted = muted ?: false, poll = poll 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 new file mode 100644 index 00000000..470675d1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt @@ -0,0 +1,87 @@ +/* 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.conversation + +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.viewdata.StatusViewData + +data class ConversationViewData( + val id: String, + val accounts: List, + val unread: Boolean, + val lastStatus: StatusViewData.Concrete +) { + fun toEntity( + accountId: Long, + favourited: Boolean = lastStatus.status.favourited, + bookmarked: Boolean = lastStatus.status.bookmarked, + muted: Boolean = lastStatus.status.muted ?: false, + poll: Poll? = lastStatus.status.poll, + expanded: Boolean = lastStatus.isExpanded, + collapsed: Boolean = lastStatus.isCollapsed, + showingHiddenContent: Boolean = lastStatus.isShowingContent + ): ConversationEntity { + return ConversationEntity( + accountId = accountId, + id = id, + accounts = accounts, + unread = unread, + lastStatus = lastStatus.toConversationStatusEntity( + favourited = favourited, + bookmarked = bookmarked, + muted = muted, + poll = poll, + expanded = expanded, + collapsed = collapsed, + showingHiddenContent = showingHiddenContent + ) + ) + } +} + +fun StatusViewData.Concrete.toConversationStatusEntity( + favourited: Boolean = status.favourited, + bookmarked: Boolean = status.bookmarked, + muted: Boolean = status.muted ?: false, + poll: Poll? = status.poll, + expanded: Boolean = isExpanded, + collapsed: Boolean = isCollapsed, + showingHiddenContent: Boolean = isShowingContent +): ConversationStatusEntity { + return ConversationStatusEntity( + id = id, + url = status.url, + inReplyToId = status.inReplyToId, + inReplyToAccountId = status.inReplyToAccountId, + account = status.account.toEntity(), + content = status.content, + createdAt = status.createdAt, + emojis = status.emojis, + favouritesCount = status.favouritesCount, + favourited = favourited, + bookmarked = bookmarked, + sensitive = status.sensitive, + spoilerText = status.spoilerText, + attachments = status.attachments, + mentions = status.mentions, + tags = status.tags, + showingHiddenContent = showingHiddenContent, + expanded = expanded, + collapsed = collapsed, + muted = muted, + poll = poll + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java index 436ba84e..ffb88a94 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java @@ -28,11 +28,14 @@ import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.adapter.StatusBaseViewHolder; import com.keylesspalace.tusky.entity.Attachment; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.entity.TimelineAccount; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.util.ImageLoadingHelper; import com.keylesspalace.tusky.util.SmartLengthInputFilter; import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.viewdata.PollViewDataKt; +import com.keylesspalace.tusky.viewdata.StatusViewData; import java.util.List; @@ -69,11 +72,12 @@ public class ConversationViewHolder extends StatusBaseViewHolder { return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height); } - void setupWithConversation(ConversationEntity conversation) { - ConversationStatusEntity status = conversation.getLastStatus(); - ConversationAccountEntity account = status.getAccount(); + void setupWithConversation(ConversationViewData conversation) { + StatusViewData.Concrete statusViewData = conversation.getLastStatus(); + Status status = statusViewData.getStatus(); + TimelineAccount account = status.getAccount(); - setupCollapsedState(status.getCollapsible(), status.getCollapsed(), status.getExpanded(), status.getSpoilerText(), listener); + setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), statusViewData.getSpoilerText(), listener); setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions); setUsername(account.getUsername()); @@ -84,7 +88,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder { List attachments = status.getAttachments(); boolean sensitive = status.getSensitive(); if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { - setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(), + setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(), statusDisplayOptions.useBlurhash()); if (attachments.size() == 0) { @@ -95,7 +99,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder { mediaLabel.setVisibility(View.GONE); } } else { - setMediaLabel(attachments, sensitive, listener, status.getShowingHiddenContent()); + setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent()); // Hide all unused views. mediaPreviews[0].setVisibility(View.GONE); mediaPreviews[1].setVisibility(View.GONE); @@ -104,10 +108,10 @@ public class ConversationViewHolder extends StatusBaseViewHolder { hideSensitiveMediaWarning(); } - setupButtons(listener, account.getId(), status.getContent().toString(), + setupButtons(listener, account.getId(), statusViewData.getContent().toString(), statusDisplayOptions); - setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(), + setSpoilerAndContent(statusViewData.isExpanded(), statusViewData.getContent(), status.getSpoilerText(), status.getMentions(), status.getTags(), status.getEmojis(), PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener); diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index a09026c2..243c3744 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -153,24 +153,24 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onFavourite(favourite: Boolean, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.favourite(favourite, conversation) } } override fun onBookmark(favourite: Boolean, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.bookmark(favourite, conversation) } } override fun onMore(view: View, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> val popup = PopupMenu(requireContext(), view) popup.inflate(R.menu.conversation_more) - if (conversation.lastStatus.muted) { + if (conversation.lastStatus.status.muted == true) { popup.menu.removeItem(R.id.status_mute_conversation) } else { popup.menu.removeItem(R.id.status_unmute_conversation) @@ -189,14 +189,14 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { - adapter.item(position)?.let { conversation -> - viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.toStatus()), view) + adapter.peek(position)?.let { conversation -> + viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.status), view) } } override fun onViewThread(position: Int) { - adapter.item(position)?.let { conversation -> - viewThread(conversation.lastStatus.id, conversation.lastStatus.url) + adapter.peek(position)?.let { conversation -> + viewThread(conversation.lastStatus.id, conversation.lastStatus.status.url) } } @@ -205,13 +205,13 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onExpandedChange(expanded: Boolean, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.expandHiddenStatus(expanded, conversation) } } override fun onContentHiddenChange(isShowing: Boolean, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.showContent(isShowing, conversation) } } @@ -221,7 +221,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.collapseLongStatus(isCollapsed, conversation) } } @@ -241,12 +241,12 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onReply(position: Int) { - adapter.item(position)?.let { conversation -> - reply(conversation.lastStatus.toStatus()) + adapter.peek(position)?.let { conversation -> + reply(conversation.lastStatus.status) } } - private fun deleteConversation(conversation: ConversationEntity) { + private fun deleteConversation(conversation: ConversationViewData) { AlertDialog.Builder(requireContext()) .setMessage(R.string.dialog_delete_conversation_warning) .setNegativeButton(android.R.string.cancel, null) @@ -268,7 +268,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onVoteInPoll(position: Int, choices: MutableList) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.voteInPoll(choices, conversation) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt index 396f8e48..9326a05c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt @@ -16,16 +16,18 @@ package com.keylesspalace.tusky.components.conversation import android.util.Log +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 androidx.paging.map import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases -import com.keylesspalace.tusky.util.RxAwareViewModel +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await import javax.inject.Inject @@ -35,7 +37,7 @@ class ConversationsViewModel @Inject constructor( private val database: AppDatabase, private val accountManager: AccountManager, private val api: MastodonApi -) : RxAwareViewModel() { +) : ViewModel() { @OptIn(ExperimentalPagingApi::class) val conversationFlow = Pager( @@ -44,104 +46,117 @@ class ConversationsViewModel @Inject constructor( pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) } ) .flow + .map { pagingData -> + pagingData.map { conversation -> conversation.toViewData() } + } .cachedIn(viewModelScope) - fun favourite(favourite: Boolean, conversation: ConversationEntity) { + fun favourite(favourite: Boolean, conversation: ConversationViewData) { viewModelScope.launch { try { timelineCases.favourite(conversation.lastStatus.id, favourite).await() - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(favourited = favourite) + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + favourited = favourite ) - database.conversationDao().insert(newConversation) + saveConversationToDb(newConversation) } catch (e: Exception) { Log.w(TAG, "failed to favourite status", e) } } } - fun bookmark(bookmark: Boolean, conversation: ConversationEntity) { + fun bookmark(bookmark: Boolean, conversation: ConversationViewData) { viewModelScope.launch { try { timelineCases.bookmark(conversation.lastStatus.id, bookmark).await() - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(bookmarked = bookmark) + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + bookmarked = bookmark ) - database.conversationDao().insert(newConversation) + saveConversationToDb(newConversation) } catch (e: Exception) { Log.w(TAG, "failed to bookmark status", e) } } } - fun voteInPoll(choices: List, conversation: ConversationEntity) { + fun voteInPoll(choices: List, conversation: ConversationViewData) { viewModelScope.launch { try { - val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.poll?.id!!, choices).await() - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(poll = poll) + val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.status.poll?.id!!, choices).await() + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + poll = poll ) - database.conversationDao().insert(newConversation) + saveConversationToDb(newConversation) } catch (e: Exception) { Log.w(TAG, "failed to vote in poll", e) } } } - fun expandHiddenStatus(expanded: Boolean, conversation: ConversationEntity) { + fun expandHiddenStatus(expanded: Boolean, conversation: ConversationViewData) { viewModelScope.launch { - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(expanded = expanded) + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + expanded = expanded ) saveConversationToDb(newConversation) } } - fun collapseLongStatus(collapsed: Boolean, conversation: ConversationEntity) { + fun collapseLongStatus(collapsed: Boolean, conversation: ConversationViewData) { viewModelScope.launch { - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(collapsed = collapsed) + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + collapsed = collapsed ) saveConversationToDb(newConversation) } } - fun showContent(showing: Boolean, conversation: ConversationEntity) { + fun showContent(showing: Boolean, conversation: ConversationViewData) { viewModelScope.launch { - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing) + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + showingHiddenContent = showing ) saveConversationToDb(newConversation) } } - fun remove(conversation: ConversationEntity) { + fun remove(conversation: ConversationViewData) { viewModelScope.launch { try { api.deleteConversation(conversationId = conversation.id) - database.conversationDao().delete(conversation) + database.conversationDao().delete( + id = conversation.id, + accountId = accountManager.activeAccount!!.id + ) } catch (e: Exception) { Log.w(TAG, "failed to delete conversation", e) } } } - fun muteConversation(conversation: ConversationEntity) { + fun muteConversation(conversation: ConversationViewData) { viewModelScope.launch { try { - val newStatus = timelineCases.muteConversation( + timelineCases.muteConversation( conversation.lastStatus.id, - !conversation.lastStatus.muted + !(conversation.lastStatus.status.muted ?: false) ).await() - val newConversation = conversation.copy( - lastStatus = newStatus.toEntity() + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + muted = !(conversation.lastStatus.status.muted ?: false) ) database.conversationDao().insert(newConversation) @@ -151,7 +166,7 @@ class ConversationsViewModel @Inject constructor( } } - suspend fun saveConversationToDb(conversation: ConversationEntity) { + private suspend fun saveConversationToDb(conversation: ConversationEntity) { database.conversationDao().insert(conversation) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt index f8991282..9f99da53 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.viewModelScope import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn +import androidx.paging.map import com.keylesspalace.tusky.appstore.BlockEvent import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.MuteEvent @@ -34,11 +35,13 @@ import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.RxAwareViewModel import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.toViewData import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject @@ -74,6 +77,11 @@ class ReportViewModel @Inject constructor( pagingSourceFactory = { StatusesPagingSource(accountId, mastodonApi) } ).flow } + .map { pagingData -> + /* TODO: refactor reports to use the isShowingContent / isExpanded / isCollapsed attributes from StatusViewData.Concrete + instead of StatusViewState */ + pagingData.map { status -> status.toViewData(false, false, false) } + } .cachedIn(viewModelScope) private val selectedIds = HashSet() @@ -155,7 +163,7 @@ class ReportViewModel @Inject constructor( .observeOn(AndroidSchedulers.mainThread()) .subscribe( { relationship -> - val muting = relationship?.muting == true + val muting = relationship.muting muteStateMutable.value = Success(muting) if (muting) { eventHub.dispatch(MuteEvent(accountId)) @@ -180,7 +188,7 @@ class ReportViewModel @Inject constructor( .observeOn(AndroidSchedulers.mainThread()) .subscribe( { relationship -> - val blocking = relationship?.blocking == true + val blocking = relationship.blocking blockStateMutable.value = Success(blocking) if (blocking) { eventHub.dispatch(BlockEvent(accountId)) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt index 1b3b0de6..9dceddec 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt @@ -37,6 +37,7 @@ import com.keylesspalace.tusky.util.setClickableMentions import com.keylesspalace.tusky.util.setClickableText import com.keylesspalace.tusky.util.shouldTrimStatus import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.toViewData import java.util.Date @@ -45,20 +46,21 @@ class StatusViewHolder( private val statusDisplayOptions: StatusDisplayOptions, private val viewState: StatusViewState, private val adapterHandler: AdapterHandler, - private val getStatusForPosition: (Int) -> Status? + private val getStatusForPosition: (Int) -> StatusViewData.Concrete? ) : RecyclerView.ViewHolder(binding.root) { + private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height) private val statusViewHelper = StatusViewHelper(itemView) private val previewListener = object : StatusViewHelper.MediaPreviewListener { override fun onViewMedia(v: View?, idx: Int) { - status()?.let { status -> - adapterHandler.showMedia(v, status, idx) + viewdata()?.let { viewdata -> + adapterHandler.showMedia(v, viewdata.status, idx) } } override fun onContentHiddenChange(isShowing: Boolean) { - status()?.id?.let { id -> + viewdata()?.id?.let { id -> viewState.setMediaShow(id, isShowing) } } @@ -66,57 +68,57 @@ class StatusViewHolder( init { binding.statusSelection.setOnCheckedChangeListener { _, isChecked -> - status()?.let { status -> - adapterHandler.setStatusChecked(status, isChecked) + viewdata()?.let { viewdata -> + adapterHandler.setStatusChecked(viewdata.status, isChecked) } } binding.statusMediaPreviewContainer.clipToOutline = true } - fun bind(status: Status) { - binding.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id) + fun bind(viewData: StatusViewData.Concrete) { + binding.statusSelection.isChecked = adapterHandler.isStatusChecked(viewData.id) updateTextView() - val sensitive = status.sensitive + val sensitive = viewData.status.sensitive statusViewHelper.setMediasPreview( - statusDisplayOptions, status.attachments, - sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive), + statusDisplayOptions, viewData.status.attachments, + sensitive, previewListener, viewState.isMediaShow(viewData.id, viewData.status.sensitive), mediaViewHeight ) - statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions) - setCreatedAt(status.createdAt) + statusViewHelper.setupPollReadonly(viewData.status.poll.toViewData(), viewData.status.emojis, statusDisplayOptions) + setCreatedAt(viewData.status.createdAt) } private fun updateTextView() { - status()?.let { status -> + viewdata()?.let { viewdata -> setupCollapsedState( - shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true), - viewState.isContentShow(status.id, status.sensitive), status.spoilerText + shouldTrimStatus(viewdata.content), viewState.isCollapsed(viewdata.id, true), + viewState.isContentShow(viewdata.id, viewdata.status.sensitive), viewdata.spoilerText ) - if (status.spoilerText.isBlank()) { - setTextVisible(true, status.content, status.mentions, status.tags, status.emojis, adapterHandler) + if (viewdata.spoilerText.isBlank()) { + setTextVisible(true, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler) binding.statusContentWarningButton.hide() binding.statusContentWarningDescription.hide() } else { - val emojiSpoiler = status.spoilerText.emojify(status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis) + val emojiSpoiler = viewdata.spoilerText.emojify(viewdata.status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis) binding.statusContentWarningDescription.text = emojiSpoiler binding.statusContentWarningDescription.show() binding.statusContentWarningButton.show() - setContentWarningButtonText(viewState.isContentShow(status.id, true)) + setContentWarningButtonText(viewState.isContentShow(viewdata.id, true)) binding.statusContentWarningButton.setOnClickListener { - status()?.let { status -> - val contentShown = viewState.isContentShow(status.id, true) + viewdata()?.let { viewdata -> + val contentShown = viewState.isContentShow(viewdata.id, true) binding.statusContentWarningDescription.invalidate() - viewState.setContentShow(status.id, !contentShown) - setTextVisible(!contentShown, status.content, status.mentions, status.tags, status.emojis, adapterHandler) + viewState.setContentShow(viewdata.id, !contentShown) + setTextVisible(!contentShown, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler) setContentWarningButtonText(!contentShown) } } - setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.tags, status.emojis, adapterHandler) + setTextVisible(viewState.isContentShow(viewdata.id, true), viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler) } } } @@ -169,8 +171,8 @@ class StatusViewHolder( /* input filter for TextViews have to be set before text */ if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { binding.buttonToggleContent.setOnClickListener { - status()?.let { status -> - viewState.setCollapsed(status.id, !collapsed) + viewdata()?.let { viewdata -> + viewState.setCollapsed(viewdata.id, !collapsed) updateTextView() } } @@ -189,5 +191,5 @@ class StatusViewHolder( } } - private fun status() = getStatusForPosition(bindingAdapterPosition) + private fun viewdata() = getStatusForPosition(bindingAdapterPosition) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt index 76ed2ebe..314513eb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt @@ -22,16 +22,16 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.components.report.model.StatusViewState import com.keylesspalace.tusky.databinding.ItemReportStatusBinding -import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.StatusViewData class StatusesAdapter( private val statusDisplayOptions: StatusDisplayOptions, private val statusViewState: StatusViewState, private val adapterHandler: AdapterHandler -) : PagingDataAdapter(STATUS_COMPARATOR) { +) : PagingDataAdapter(STATUS_COMPARATOR) { - private val statusForPosition: (Int) -> Status? = { position: Int -> + private val statusForPosition: (Int) -> StatusViewData.Concrete? = { position: Int -> if (position != RecyclerView.NO_POSITION) getItem(position) else null } @@ -50,11 +50,11 @@ class StatusesAdapter( } companion object { - val STATUS_COMPARATOR = object : DiffUtil.ItemCallback() { - override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean = + val STATUS_COMPARATOR = object : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean = oldItem == newItem - override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean = + override fun areItemsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean = oldItem.id == newItem.id } } 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 252b9880..6ec95423 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 @@ -15,9 +15,6 @@ package com.keylesspalace.tusky.components.timeline -import android.text.SpannedString -import androidx.core.text.parseAsHtml -import androidx.core.text.toHtml import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.keylesspalace.tusky.db.TimelineAccountEntity @@ -29,8 +26,6 @@ import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.TimelineAccount -import com.keylesspalace.tusky.util.shouldTrimStatus -import com.keylesspalace.tusky.util.trimTrailingWhitespace import com.keylesspalace.tusky.viewdata.StatusViewData import java.util.Date @@ -119,7 +114,7 @@ fun Status.toEntity( authorServerId = actionableStatus.account.id, inReplyToId = actionableStatus.inReplyToId, inReplyToAccountId = actionableStatus.inReplyToAccountId, - content = actionableStatus.content.toHtml(), + content = actionableStatus.content, createdAt = actionableStatus.createdAt.time, emojis = actionableStatus.emojis.let(gson::toJson), reblogsCount = actionableStatus.reblogsCount, @@ -165,8 +160,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { inReplyToId = status.inReplyToId, inReplyToAccountId = status.inReplyToAccountId, reblog = null, - content = status.content?.parseAsHtml()?.trimTrailingWhitespace() - ?: SpannedString(""), + content = status.content.orEmpty(), createdAt = Date(status.createdAt), emojis = emojis, reblogsCount = status.reblogsCount, @@ -195,7 +189,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { inReplyToId = null, inReplyToAccountId = null, reblog = reblog, - content = SpannedString(""), + content = "", createdAt = Date(status.createdAt), // lie but whatever? emojis = listOf(), reblogsCount = 0, @@ -223,8 +217,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { inReplyToId = status.inReplyToId, inReplyToAccountId = status.inReplyToAccountId, reblog = null, - content = status.content?.parseAsHtml()?.trimTrailingWhitespace() - ?: SpannedString(""), + content = status.content.orEmpty(), createdAt = Date(status.createdAt), emojis = emojis, reblogsCount = status.reblogsCount, @@ -249,7 +242,6 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { status = status, isExpanded = this.status.expanded, isShowingContent = this.status.contentShowing, - isCollapsible = shouldTrimStatus(status.content), isCollapsed = this.status.contentCollapsed ) } 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 304b4e5a..7158a7b3 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 @@ -42,7 +42,10 @@ import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.viewdata.StatusViewData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await @@ -79,15 +82,13 @@ class CachedTimelineViewModel @Inject constructor( } ).flow .map { pagingData -> - pagingData.map { timelineStatus -> + pagingData.map(Dispatchers.Default.asExecutor()) { timelineStatus -> timelineStatus.toViewData(gson) - } - } - .map { pagingData -> - pagingData.filter { statusViewData -> + }.filter(Dispatchers.Default.asExecutor()) { statusViewData -> !shouldFilterStatus(statusViewData) } } + .flowOn(Dispatchers.Default) .cachedIn(viewModelScope) init { 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 f70fdcc8..ca7988bb 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 @@ -40,6 +40,9 @@ import com.keylesspalace.tusky.util.isLessThan import com.keylesspalace.tusky.util.isLessThanOrEqual import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.viewdata.StatusViewData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await @@ -79,10 +82,11 @@ class NetworkTimelineViewModel @Inject constructor( remoteMediator = NetworkTimelineRemoteMediator(accountManager, this) ).flow .map { pagingData -> - pagingData.filter { statusViewData -> + pagingData.filter(Dispatchers.Default.asExecutor()) { statusViewData -> !shouldFilterStatus(statusViewData) } } + .flowOn(Dispatchers.Default) .cachedIn(viewModelScope) override fun updatePoll(newPoll: Poll, status: 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 2131300c..c541958a 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 = 32) + }, version = 33) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -490,4 +490,41 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSignUps` INTEGER NOT NULL DEFAULT 1"); } }; + + public static final Migration MIGRATION_32_33 = new Migration(32, 33) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + + // ConversationEntity lost the s_collapsible column + // since SQLite does not support removing columns and it is just a cache table, we recreate the whole table. + database.execSQL("DROP TABLE `ConversationEntity`"); + database.execSQL("CREATE TABLE IF NOT EXISTS `ConversationEntity` (" + + "`accountId` INTEGER NOT NULL," + + "`id` TEXT NOT NULL," + + "`accounts` TEXT NOT NULL," + + "`unread` INTEGER NOT NULL," + + "`s_id` TEXT NOT NULL," + + "`s_url` TEXT," + + "`s_inReplyToId` TEXT," + + "`s_inReplyToAccountId` TEXT," + + "`s_account` TEXT NOT NULL," + + "`s_content` TEXT NOT NULL," + + "`s_createdAt` INTEGER NOT NULL," + + "`s_emojis` TEXT NOT NULL," + + "`s_favouritesCount` INTEGER NOT NULL," + + "`s_favourited` INTEGER NOT NULL," + + "`s_bookmarked` INTEGER NOT NULL," + + "`s_sensitive` INTEGER NOT NULL," + + "`s_spoilerText` TEXT NOT NULL," + + "`s_attachments` TEXT NOT NULL," + + "`s_mentions` TEXT NOT NULL," + + "`s_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`))"); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt index 393a2392..fe093bd0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt @@ -17,7 +17,6 @@ package com.keylesspalace.tusky.db import androidx.paging.PagingSource import androidx.room.Dao -import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query @@ -31,8 +30,8 @@ interface ConversationsDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(conversation: ConversationEntity): Long - @Delete - suspend fun delete(conversation: ConversationEntity): Int + @Query("DELETE FROM ConversationEntity WHERE id = :id AND accountId = :accountId") + suspend fun delete(id: String, accountId: Long): Int @Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC") fun conversationsForAccount(accountId: Long): PagingSource diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt index c9daec0a..34ff6474 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -15,9 +15,6 @@ package com.keylesspalace.tusky.db -import android.text.Spanned -import androidx.core.text.parseAsHtml -import androidx.core.text.toHtml import androidx.room.ProvidedTypeConverter import androidx.room.TypeConverter import com.google.gson.Gson @@ -31,10 +28,8 @@ import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status -import com.keylesspalace.tusky.util.trimTrailingWhitespace import java.net.URLDecoder import java.net.URLEncoder -import java.util.ArrayList import java.util.Date import javax.inject.Inject import javax.inject.Singleton @@ -140,22 +135,6 @@ class Converters @Inject constructor ( return Date(date) } - @TypeConverter - fun spannedToString(spanned: Spanned?): String? { - if (spanned == null) { - return null - } - return spanned.toHtml() - } - - @TypeConverter - fun stringToSpanned(spannedString: String?): Spanned? { - if (spannedString == null) { - return null - } - return spannedString.parseAsHtml().trimTrailingWhitespace() - } - @TypeConverter fun pollToJson(poll: Poll?): String? { return gson.toJson(poll) 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 677f8167..7f0fbd01 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -63,6 +63,7 @@ class AppModule { AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")), AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29, AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32, + AppDatabase.MIGRATION_32_33 ) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index d927c299..d8b52ca3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -18,13 +18,10 @@ package com.keylesspalace.tusky.di import android.content.Context import android.content.SharedPreferences import android.os.Build -import android.text.Spanned import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory import com.google.gson.Gson -import com.google.gson.GsonBuilder import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.db.AccountManager -import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.getNonNullString @@ -52,11 +49,7 @@ class NetworkModule { @Provides @Singleton - fun providesGson(): Gson { - return GsonBuilder() - .registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter()) - .create() - } + fun providesGson() = Gson() @Provides @Singleton diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt index 672bd5aa..bf5431ee 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt @@ -15,7 +15,6 @@ package com.keylesspalace.tusky.entity -import android.text.Spanned import com.google.gson.annotations.SerializedName import java.util.Date @@ -24,7 +23,7 @@ data class Account( @SerializedName("username") val localUsername: String, @SerializedName("acct") val username: String, @SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract - val note: Spanned, + val note: String, val url: String, val avatar: String, val header: String, @@ -46,56 +45,6 @@ data class Account( } else displayName fun isRemote(): Boolean = this.username != this.localUsername - - /** - * overriding equals & hashcode because Spanned does not always compare correctly otherwise - */ - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as Account - - if (id != other.id) return false - if (localUsername != other.localUsername) return false - if (username != other.username) return false - if (displayName != other.displayName) return false - if (note.toString() != other.note.toString()) return false - if (url != other.url) return false - if (avatar != other.avatar) return false - if (header != other.header) return false - if (locked != other.locked) return false - if (followersCount != other.followersCount) return false - if (followingCount != other.followingCount) return false - if (statusesCount != other.statusesCount) return false - if (source != other.source) return false - if (bot != other.bot) return false - if (emojis != other.emojis) return false - if (fields != other.fields) return false - if (moved != other.moved) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + localUsername.hashCode() - result = 31 * result + username.hashCode() - result = 31 * result + (displayName?.hashCode() ?: 0) - result = 31 * result + note.toString().hashCode() - result = 31 * result + url.hashCode() - result = 31 * result + avatar.hashCode() - result = 31 * result + header.hashCode() - result = 31 * result + locked.hashCode() - result = 31 * result + followersCount - result = 31 * result + followingCount - result = 31 * result + statusesCount - result = 31 * result + (source?.hashCode() ?: 0) - result = 31 * result + bot.hashCode() - result = 31 * result + (emojis?.hashCode() ?: 0) - result = 31 * result + (fields?.hashCode() ?: 0) - result = 31 * result + (moved?.hashCode() ?: 0) - return result - } } data class AccountSource( @@ -107,7 +56,7 @@ data class AccountSource( data class Field( val name: String, - val value: Spanned, + val value: String, @SerializedName("verified_at") val verifiedAt: Date? ) 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 400e9764..00d5659d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt @@ -15,13 +15,12 @@ package com.keylesspalace.tusky.entity -import android.text.Spanned import com.google.gson.annotations.SerializedName import java.util.Date data class Announcement( val id: String, - val content: Spanned, + val content: String, @SerializedName("starts_at") val startsAt: Date?, @SerializedName("ends_at") val endsAt: Date?, @SerializedName("all_day") val allDay: Boolean, diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt index 52011f3d..29fe7f8e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt @@ -15,13 +15,12 @@ package com.keylesspalace.tusky.entity -import android.text.Spanned import com.google.gson.annotations.SerializedName data class Card( val url: String, - val title: Spanned, - val description: Spanned, + val title: String, + val description: String, @SerializedName("author_name") val authorName: String, val image: String, val type: String, @@ -31,9 +30,7 @@ data class Card( val embed_url: String? ) { - override fun hashCode(): Int { - return url.hashCode() - } + override fun hashCode() = url.hashCode() override fun equals(other: Any?): Boolean { if (other !is Card) { 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 f75ce4e7..19cb7aa6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -16,9 +16,9 @@ package com.keylesspalace.tusky.entity import android.text.SpannableStringBuilder -import android.text.Spanned import android.text.style.URLSpan import com.google.gson.annotations.SerializedName +import com.keylesspalace.tusky.util.parseAsMastodonHtml import java.util.ArrayList import java.util.Date @@ -29,7 +29,7 @@ data class Status( @SerializedName("in_reply_to_id") var inReplyToId: String?, @SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?, val reblog: Status?, - val content: Spanned, + val content: String, @SerializedName("created_at") val createdAt: Date, val emojis: List, @SerializedName("reblogs_count") val reblogsCount: Int, @@ -134,8 +134,9 @@ data class Status( } private fun getEditableText(): String { - val builder = SpannableStringBuilder(content) - for (span in content.getSpans(0, content.length, URLSpan::class.java)) { + val contentSpanned = content.parseAsMastodonHtml() + val builder = SpannableStringBuilder(content.parseAsMastodonHtml()) + for (span in contentSpanned.getSpans(0, content.length, URLSpan::class.java)) { val url = span.url for ((_, url1, username) in mentions) { if (url == url1) { @@ -149,71 +150,6 @@ data class Status( return builder.toString() } - /** - * overriding equals & hashcode because Spanned does not always compare correctly otherwise - */ - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as Status - - if (id != other.id) return false - if (url != other.url) return false - if (account != other.account) return false - if (inReplyToId != other.inReplyToId) return false - if (inReplyToAccountId != other.inReplyToAccountId) return false - if (reblog != other.reblog) return false - if (content.toString() != other.content.toString()) return false - if (createdAt != other.createdAt) return false - if (emojis != other.emojis) return false - if (reblogsCount != other.reblogsCount) return false - if (favouritesCount != other.favouritesCount) return false - if (reblogged != other.reblogged) return false - if (favourited != other.favourited) return false - if (bookmarked != other.bookmarked) return false - if (sensitive != other.sensitive) return false - if (spoilerText != other.spoilerText) return false - if (visibility != other.visibility) return false - if (attachments != other.attachments) return false - if (mentions != other.mentions) return false - if (tags != other.tags) return false - if (application != other.application) return false - if (pinned != other.pinned) return false - if (muted != other.muted) return false - if (poll != other.poll) return false - if (card != other.card) return false - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + (url?.hashCode() ?: 0) - result = 31 * result + account.hashCode() - result = 31 * result + (inReplyToId?.hashCode() ?: 0) - result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0) - result = 31 * result + (reblog?.hashCode() ?: 0) - result = 31 * result + content.toString().hashCode() - result = 31 * result + createdAt.hashCode() - result = 31 * result + emojis.hashCode() - result = 31 * result + reblogsCount - result = 31 * result + favouritesCount - result = 31 * result + reblogged.hashCode() - result = 31 * result + favourited.hashCode() - result = 31 * result + bookmarked.hashCode() - result = 31 * result + sensitive.hashCode() - result = 31 * result + spoilerText.hashCode() - result = 31 * result + visibility.hashCode() - result = 31 * result + attachments.hashCode() - result = 31 * result + mentions.hashCode() - result = 31 * result + (tags?.hashCode() ?: 0) - result = 31 * result + (application?.hashCode() ?: 0) - result = 31 * result + (pinned?.hashCode() ?: 0) - result = 31 * result + (muted?.hashCode() ?: 0) - result = 31 * result + (poll?.hashCode() ?: 0) - result = 31 * result + (card?.hashCode() ?: 0) - return result - } - data class Mention( val id: String, val url: String, diff --git a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt deleted file mode 100644 index 60af6134..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* Copyright 2020 Tusky Contributors - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.json - -import android.text.Spanned -import android.text.SpannedString -import androidx.core.text.HtmlCompat -import androidx.core.text.parseAsHtml -import androidx.core.text.toHtml -import com.google.gson.JsonDeserializationContext -import com.google.gson.JsonDeserializer -import com.google.gson.JsonElement -import com.google.gson.JsonParseException -import com.google.gson.JsonPrimitive -import com.google.gson.JsonSerializationContext -import com.google.gson.JsonSerializer -import com.keylesspalace.tusky.util.trimTrailingWhitespace -import java.lang.reflect.Type - -class SpannedTypeAdapter : JsonDeserializer, JsonSerializer { - @Throws(JsonParseException::class) - override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Spanned { - return json.asString - /* Mastodon uses 'white-space: pre-wrap;' so spaces are displayed as returned by the Api. - * We can't use CSS so we replace spaces with non-breaking-spaces to emulate the behavior. - */ - ?.replace("
", "
 ") - ?.replace("
", "
 ") - ?.replace("
", "
 ") - ?.replace(" ", "  ") - ?.parseAsHtml() - /* Html.fromHtml returns trailing whitespace if the html ends in a

tag, which - * most status contents do, so it should be trimmed. */ - ?.trimTrailingWhitespace() - ?: SpannedString("") - } - - override fun serialize(src: Spanned?, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { - return JsonPrimitive(src!!.toHtml(HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)) - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt new file mode 100644 index 00000000..fc62c78d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.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.util + +import android.text.SpannableStringBuilder +import android.text.Spanned +import androidx.core.text.parseAsHtml + +/** + * parse a String containing html from the Mastodon api to Spanned + */ +fun String.parseAsMastodonHtml(): Spanned { + return this.replace("
", "
 ") + .replace("
", "
 ") + .replace("
", "
 ") + .replace(" ", "  ") + .parseAsHtml() + /* Html.fromHtml returns trailing whitespace if the html ends in a

tag, which + * most status contents do, so it should be trimmed. */ + .trimTrailingWhitespace() +} + +fun replaceCrashingCharacters(content: Spanned): Spanned { + return replaceCrashingCharacters(content as CharSequence) as Spanned +} + +fun replaceCrashingCharacters(content: CharSequence): CharSequence? { + var replacing = false + var builder: SpannableStringBuilder? = null + val length = content.length + for (index in 0 until length) { + val character = content[index] + + // If there are more than one or two, switch to a map + if (character == SOFT_HYPHEN) { + if (!replacing) { + replacing = true + builder = SpannableStringBuilder(content, 0, index) + } + builder!!.append(ASCII_HYPHEN) + } else if (replacing) { + builder!!.append(character) + } + } + return if (replacing) builder else content +} + +private const val SOFT_HYPHEN = '\u00ad' +private const val ASCII_HYPHEN = '-' 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 52d9713f..fef9c0bb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt @@ -27,12 +27,9 @@ fun Status.toViewData( isExpanded: Boolean, isCollapsed: Boolean ): StatusViewData.Concrete { - val visibleStatus = this.reblog ?: this - return StatusViewData.Concrete( status = this, isShowingContent = isShowingContent, - isCollapsible = shouldTrimStatus(visibleStatus.content), isCollapsed = isCollapsed, isExpanded = isExpanded, ) 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 d8f27157..8ac212d9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt @@ -15,9 +15,11 @@ package com.keylesspalace.tusky.viewdata import android.os.Build -import android.text.SpannableStringBuilder import android.text.Spanned import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.util.parseAsMastodonHtml +import com.keylesspalace.tusky.util.replaceCrashingCharacters +import com.keylesspalace.tusky.util.shouldTrimStatus /** * Created by charlag on 11/07/2017. @@ -32,13 +34,6 @@ sealed class StatusViewData { val status: Status, val isExpanded: Boolean, val isShowingContent: Boolean, - /** - * Specifies whether the content of this post is allowed to be collapsed or if it should show - * all content regardless. - * - * @return Whether the post is collapsible or never collapsed. - */ - val isCollapsible: Boolean, /** * Specifies whether the content of this post is currently limited in visibility to the first * 500 characters or not. @@ -51,6 +46,14 @@ sealed class StatusViewData { override val id: String get() = status.id + /** + * Specifies whether the content of this post is allowed to be collapsed or if it should show + * all content regardless. + * + * @return Whether the post is collapsible or never collapsed. + */ + val isCollapsible: Boolean + val content: Spanned val spoilerText: String val username: String @@ -74,45 +77,17 @@ sealed class StatusViewData { init { if (Build.VERSION.SDK_INT == 23) { // https://github.com/tuskyapp/Tusky/issues/563 - this.content = replaceCrashingCharacters(status.actionableStatus.content) + this.content = replaceCrashingCharacters(status.actionableStatus.content.parseAsMastodonHtml()) this.spoilerText = replaceCrashingCharacters(status.actionableStatus.spoilerText).toString() this.username = replaceCrashingCharacters(status.actionableStatus.account.username).toString() } else { - this.content = status.actionableStatus.content + this.content = status.actionableStatus.content.parseAsMastodonHtml() this.spoilerText = status.actionableStatus.spoilerText this.username = status.actionableStatus.account.username } - } - - companion object { - private const val SOFT_HYPHEN = '\u00ad' - private const val ASCII_HYPHEN = '-' - fun replaceCrashingCharacters(content: Spanned): Spanned { - return replaceCrashingCharacters(content as CharSequence) as Spanned - } - - fun replaceCrashingCharacters(content: CharSequence): CharSequence? { - var replacing = false - var builder: SpannableStringBuilder? = null - val length = content.length - for (index in 0 until length) { - val character = content[index] - - // If there are more than one or two, switch to a map - if (character == SOFT_HYPHEN) { - if (!replacing) { - replacing = true - builder = SpannableStringBuilder(content, 0, index) - } - builder!!.append(ASCII_HYPHEN) - } else if (replacing) { - builder!!.append(character) - } - } - return if (replacing) builder else content - } + this.isCollapsible = shouldTrimStatus(this.content) } /** Helper for Java */ diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt index ff208823..beb6af9b 100644 --- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt @@ -15,7 +15,6 @@ package com.keylesspalace.tusky -import android.text.SpannedString import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.keylesspalace.tusky.entity.SearchResult import com.keylesspalace.tusky.entity.Status @@ -70,7 +69,7 @@ class BottomSheetActivityTest { inReplyToId = null, inReplyToAccountId = null, reblog = null, - content = SpannedString("omgwat"), + content = "omgwat", createdAt = Date(), emojis = emptyList(), reblogsCount = 0, diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index dc4a412f..5396a21e 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -17,7 +17,6 @@ package com.keylesspalace.tusky import android.content.Intent import android.os.Looper.getMainLooper -import android.text.SpannedString import android.widget.EditText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.keylesspalace.tusky.components.compose.ComposeActivity @@ -469,7 +468,7 @@ class ComposeActivityTest { "admin", "admin", "admin", - SpannedString(""), + "", "https://example.token", "", "", diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt index d5063943..91ea38d3 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt @@ -1,6 +1,5 @@ package com.keylesspalace.tusky -import android.text.SpannedString import androidx.test.ext.junit.runners.AndroidJUnit4 import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Filter @@ -162,7 +161,7 @@ class FilterTest { inReplyToId = null, inReplyToAccountId = null, reblog = null, - content = SpannedString(content), + content = content, createdAt = Date(), emojis = emptyList(), reblogsCount = 0, diff --git a/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt b/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt index ed06e27c..3086036a 100644 --- a/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt @@ -1,10 +1,8 @@ package com.keylesspalace.tusky -import android.text.Spanned import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.gson.GsonBuilder +import com.google.gson.Gson import com.keylesspalace.tusky.entity.Status -import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.viewdata.StatusViewData import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals @@ -39,9 +37,7 @@ class StatusComparisonTest { assertEquals(createStatus(note = "Test"), createStatus(note = "Test 123456")) } - private val gson = GsonBuilder().registerTypeAdapter( - Spanned::class.java, SpannedTypeAdapter() - ).create() + private val gson = Gson() @Test fun `two equal status view data - should be equal`() { @@ -49,14 +45,12 @@ class StatusComparisonTest { status = createStatus(), isExpanded = false, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) val viewdata2 = StatusViewData.Concrete( status = createStatus(), isExpanded = false, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) assertEquals(viewdata1, viewdata2) @@ -68,14 +62,12 @@ class StatusComparisonTest { status = createStatus(), isExpanded = true, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) val viewdata2 = StatusViewData.Concrete( status = createStatus(), isExpanded = false, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) assertNotEquals(viewdata1, viewdata2) @@ -87,14 +79,12 @@ class StatusComparisonTest { status = createStatus(content = "whatever"), isExpanded = true, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) val viewdata2 = StatusViewData.Concrete( status = createStatus(), isExpanded = false, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) assertNotEquals(viewdata1, viewdata2) diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt index 60dda419..33215e67 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt @@ -1,14 +1,19 @@ package com.keylesspalace.tusky.components.timeline import androidx.paging.PagingSource +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelinePagingSource import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Test +import org.junit.runner.RunWith import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.robolectric.annotation.Config +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) class NetworkTimelinePagingSourceTest { private val status = mockStatusViewData() 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 f7c998b5..cc6a90bd 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 @@ -1,6 +1,5 @@ package com.keylesspalace.tusky.components.timeline -import android.text.SpannedString import com.google.gson.Gson import com.keylesspalace.tusky.db.TimelineStatusWithAccount import com.keylesspalace.tusky.entity.Status @@ -25,7 +24,7 @@ fun mockStatus(id: String = "100") = Status( inReplyToId = null, inReplyToAccountId = null, reblog = null, - content = SpannedString("Test"), + content = "Test", createdAt = fixedDate, emojis = emptyList(), reblogsCount = 1, @@ -50,7 +49,6 @@ fun mockStatusViewData(id: String = "100") = StatusViewData.Concrete( status = mockStatus(id), isExpanded = false, isShowingContent = false, - isCollapsible = false, isCollapsed = true, ) From 027b659d1c0ecfcfa53607fffdd8fba0e98cbae6 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 16 Apr 2022 09:44:05 +0200 Subject: [PATCH 007/104] fix notifications showing unparsed html (#2436) --- .../tusky/components/notifications/NotificationHelper.java | 7 ++++--- .../com/keylesspalace/tusky/util/StatusParsingHelper.kt | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) 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 63c17082..83682ab2 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 @@ -16,6 +16,7 @@ package com.keylesspalace.tusky.components.notifications; +import static com.keylesspalace.tusky.util.StatusParsingHelper.parseAsMastodonHtml; import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; import android.app.NotificationChannel; @@ -341,7 +342,7 @@ public class NotificationHelper { Status status = body.getStatus(); String citedLocalAuthor = status.getAccount().getLocalUsername(); - String citedText = status.getContent().toString(); + String citedText = parseAsMastodonHtml(status.getContent()).toString(); String inReplyToId = status.getId(); Status actionableStatus = status.getActionableStatus(); Status.Visibility replyVisibility = actionableStatus.getVisibility(); @@ -690,13 +691,13 @@ public class NotificationHelper { if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) { return notification.getStatus().getSpoilerText(); } else { - return notification.getStatus().getContent().toString(); + return parseAsMastodonHtml(notification.getStatus().getContent()).toString(); } case POLL: if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText())) { return notification.getStatus().getSpoilerText(); } else { - StringBuilder builder = new StringBuilder(notification.getStatus().getContent()); + StringBuilder builder = new StringBuilder(parseAsMastodonHtml(notification.getStatus().getContent())); builder.append('\n'); Poll poll = notification.getStatus().getPoll(); List options = poll.getOptions(); diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt index fc62c78d..2ac4782c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt @@ -13,6 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ +@file:JvmName("StatusParsingHelper") package com.keylesspalace.tusky.util import android.text.SpannableStringBuilder From f2fc87a79ee7bb22693b24f54ef05a5244ca0f58 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 16 Apr 2022 09:44:37 +0200 Subject: [PATCH 008/104] upgrade Kotlin and Coroutines (#2434) --- app/build.gradle | 4 +--- build.gradle | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3c5cb162..6ed56f42 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -88,7 +88,7 @@ android { } } -ext.coroutinesVersion = "1.6.0" +ext.coroutinesVersion = "1.6.1" ext.lifecycleVersion = "2.4.1" ext.roomVersion = '2.4.2' ext.retrofitVersion = '2.9.0' @@ -99,8 +99,6 @@ ext.materialdrawerVersion = '8.4.5' // if libraries are changed here, they should also be changed in LicenseActivity dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-rx3:$coroutinesVersion" diff --git a/build.gradle b/build.gradle index c9311701..ace9a117 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,4 @@ buildscript { - ext.kotlin_version = '1.6.10' repositories { google() mavenCentral() @@ -7,7 +6,7 @@ buildscript { } dependencies { classpath "com.android.tools.build:gradle:7.1.2" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.20" classpath "org.jlleitschuh.gradle:ktlint-gradle:10.1.0" } } From 216f094e983018c27689835e277bd2611ab2bf9d Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 16 Apr 2022 09:45:45 +0200 Subject: [PATCH 009/104] upgrade ktlint gradle plugin to 10.2.1 (#2435) --- .../main/java/com/keylesspalace/tusky/ViewMediaActivity.kt | 1 - build.gradle | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt index 64d29577..fda2c82b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -283,7 +283,6 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener } return@fromCallable false } - .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .doOnDispose { diff --git a/build.gradle b/build.gradle index ace9a117..3a5251fa 100644 --- a/build.gradle +++ b/build.gradle @@ -7,11 +7,11 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:7.1.2" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.20" - classpath "org.jlleitschuh.gradle:ktlint-gradle:10.1.0" + classpath "org.jlleitschuh.gradle:ktlint-gradle:10.2.1" } } plugins { - id "org.jlleitschuh.gradle.ktlint" version "10.1.0" + id "org.jlleitschuh.gradle.ktlint" version "10.2.1" } allprojects { From e0abcbfada6a5265331d5ce78f1f20e17984e23e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9lanie=20Chauvel?= Date: Mon, 18 Apr 2022 21:41:18 +0200 Subject: [PATCH 010/104] Improve time format of posts when using absolute time (#2413) * Improve time format of posts when using absolute time * fix AbsoluteTimeFormatter, add tests * fix tests Co-authored-by: Conny Duck --- .../tusky/adapter/NotificationsAdapter.java | 30 ++++------ .../tusky/adapter/StatusBaseViewHolder.java | 27 ++------- .../report/adapter/StatusViewHolder.kt | 4 +- .../tusky/util/AbsoluteTimeFormatter.kt | 59 +++++++++++++++++++ .../tusky/util/StatusViewHelper.kt | 22 +------ .../tusky/util/AbsoluteTimeFormatterTest.kt | 46 +++++++++++++++ 6 files changed, 127 insertions(+), 61 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatter.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatterTest.kt diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 936f20a3..c1f78b46 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -47,6 +47,7 @@ import com.keylesspalace.tusky.entity.TimelineAccount; import com.keylesspalace.tusky.interfaces.AccountActionListener; import com.keylesspalace.tusky.interfaces.LinkListener; import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.AbsoluteTimeFormatter; import com.keylesspalace.tusky.util.CardViewMode; import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper; @@ -58,10 +59,8 @@ import com.keylesspalace.tusky.util.TimestampUtils; import com.keylesspalace.tusky.viewdata.NotificationViewData; import com.keylesspalace.tusky.viewdata.StatusViewData; -import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; -import java.util.Locale; import at.connyduck.sparkbutton.helpers.Utils; @@ -90,6 +89,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { private NotificationActionListener notificationActionListener; private AccountActionListener accountActionListener; private AdapterDataSource dataSource; + private final AbsoluteTimeFormatter absoluteTimeFormatter = new AbsoluteTimeFormatter(); public NotificationsAdapter(String accountId, AdapterDataSource dataSource, @@ -119,7 +119,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { case VIEW_TYPE_STATUS_NOTIFICATION: { View view = inflater .inflate(R.layout.item_status_notification, parent, false); - return new StatusNotificationViewHolder(view, statusDisplayOptions); + return new StatusNotificationViewHolder(view, statusDisplayOptions, absoluteTimeFormatter); } case VIEW_TYPE_FOLLOW: { View view = inflater @@ -383,19 +383,22 @@ public class NotificationsAdapter extends RecyclerView.Adapter { private final Button contentWarningButton; private final Button contentCollapseButton; // TODO: This code SHOULD be based on StatusBaseViewHolder private StatusDisplayOptions statusDisplayOptions; + private final AbsoluteTimeFormatter absoluteTimeFormatter; private String accountId; private String notificationId; private NotificationActionListener notificationActionListener; private StatusViewData.Concrete statusViewData; - private SimpleDateFormat shortSdf; - private SimpleDateFormat longSdf; private int avatarRadius48dp; private int avatarRadius36dp; private int avatarRadius24dp; - StatusNotificationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions) { + StatusNotificationViewHolder( + View itemView, + StatusDisplayOptions statusDisplayOptions, + AbsoluteTimeFormatter absoluteTimeFormatter + ) { super(itemView); message = itemView.findViewById(R.id.notification_top_text); statusNameBar = itemView.findViewById(R.id.status_name_bar); @@ -409,6 +412,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { contentWarningButton = itemView.findViewById(R.id.notification_content_warning_button); contentCollapseButton = itemView.findViewById(R.id.button_toggle_notification_content); this.statusDisplayOptions = statusDisplayOptions; + this.absoluteTimeFormatter = absoluteTimeFormatter; int darkerFilter = Color.rgb(123, 123, 123); statusAvatar.setColorFilter(darkerFilter, PorterDuff.Mode.MULTIPLY); @@ -417,8 +421,6 @@ public class NotificationsAdapter extends RecyclerView.Adapter { itemView.setOnClickListener(this); message.setOnClickListener(this); statusContent.setOnClickListener(this); - shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); - longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()); this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp); this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp); @@ -448,17 +450,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { protected void setCreatedAt(@Nullable Date createdAt) { if (statusDisplayOptions.useAbsoluteTime()) { - String time; - if (createdAt != null) { - if (System.currentTimeMillis() - createdAt.getTime() > 86400000L) { - time = longSdf.format(createdAt); - } else { - time = shortSdf.format(createdAt); - } - } else { - time = "??:??:??"; - } - timestampInfo.setText(time); + timestampInfo.setText(absoluteTimeFormatter.format(createdAt, true)); } else { // This is the visible timestampInfo. String readout; 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 dbca518a..c2729aa5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -40,6 +40,7 @@ import com.keylesspalace.tusky.entity.Emoji; 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.CardViewMode; import com.keylesspalace.tusky.util.CustomEmojiHelper; import com.keylesspalace.tusky.util.ImageLoadingHelper; @@ -54,10 +55,8 @@ import com.keylesspalace.tusky.viewdata.PollViewDataKt; import com.keylesspalace.tusky.viewdata.StatusViewData; import java.text.NumberFormat; -import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; -import java.util.Locale; import at.connyduck.sparkbutton.SparkButton; import at.connyduck.sparkbutton.helpers.Utils; @@ -103,10 +102,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private TextView cardUrl; private PollAdapter pollAdapter; - private SimpleDateFormat shortSdf; - private SimpleDateFormat longSdf; - private final NumberFormat numberFormat = NumberFormat.getNumberInstance(); + private final AbsoluteTimeFormatter absoluteTimeFormatter = new AbsoluteTimeFormatter(); protected int avatarRadius48dp; private int avatarRadius36dp; @@ -170,9 +167,6 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext())); ((DefaultItemAnimator) pollOptions.getItemAnimator()).setSupportsChangeAnimations(false); - this.shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()); - this.longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()); - this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp); this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp); this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp); @@ -320,7 +314,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { protected void setCreatedAt(Date createdAt, StatusDisplayOptions statusDisplayOptions) { if (statusDisplayOptions.useAbsoluteTime()) { - timestampInfo.setText(getAbsoluteTime(createdAt)); + timestampInfo.setText(absoluteTimeFormatter.format(createdAt, true)); } else { if (createdAt == null) { timestampInfo.setText("?m"); @@ -333,21 +327,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } } - private String getAbsoluteTime(Date createdAt) { - if (createdAt == null) { - return "??:??:??"; - } - if (DateUtils.isToday(createdAt.getTime())) { - return shortSdf.format(createdAt); - } else { - return longSdf.format(createdAt); - } - } - private CharSequence getCreatedAtDescription(Date createdAt, StatusDisplayOptions statusDisplayOptions) { if (statusDisplayOptions.useAbsoluteTime()) { - return getAbsoluteTime(createdAt); + return absoluteTimeFormatter.format(createdAt, true); } else { /* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m" * as 17 meters instead of minutes. */ @@ -1028,7 +1011,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { return votesText; } else { if (statusDisplayOptions.useAbsoluteTime()) { - pollDurationInfo = context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.getExpiresAt())); + pollDurationInfo = context.getString(R.string.poll_info_time_absolute, absoluteTimeFormatter.format(poll.getExpiresAt(), false)); } else { pollDurationInfo = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp); } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt index 9dceddec..82dbf163 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt @@ -26,6 +26,7 @@ import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.util.AbsoluteTimeFormatter import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.StatusViewHelper import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER @@ -51,6 +52,7 @@ class StatusViewHolder( private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height) private val statusViewHelper = StatusViewHelper(itemView) + private val absoluteTimeFormatter = AbsoluteTimeFormatter() private val previewListener = object : StatusViewHelper.MediaPreviewListener { override fun onViewMedia(v: View?, idx: Int) { @@ -154,7 +156,7 @@ class StatusViewHolder( private fun setCreatedAt(createdAt: Date?) { if (statusDisplayOptions.useAbsoluteTime) { - binding.timestampInfo.text = statusViewHelper.getAbsoluteTime(createdAt) + binding.timestampInfo.text = absoluteTimeFormatter.format(createdAt) } else { binding.timestampInfo.text = if (createdAt != null) { val then = createdAt.time diff --git a/app/src/main/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatter.kt b/app/src/main/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatter.kt new file mode 100644 index 00000000..7d46388b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatter.kt @@ -0,0 +1,59 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util + +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +class AbsoluteTimeFormatter @JvmOverloads constructor(private val tz: TimeZone = TimeZone.getDefault()) { + private val sameDaySdf = SimpleDateFormat("HH:mm", Locale.getDefault()).apply { this.timeZone = tz } + private val sameYearSdf = SimpleDateFormat("MM-dd HH:mm", Locale.getDefault()).apply { this.timeZone = tz } + private val otherYearSdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).apply { this.timeZone = tz } + private val otherYearCompleteSdf = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).apply { this.timeZone = tz } + + @JvmOverloads + fun format(time: Date?, shortFormat: Boolean = true, now: Date = Date()): String { + return when { + time == null -> "??" + isSameDate(time, now, tz) -> sameDaySdf.format(time) + isSameYear(time, now, tz) -> sameYearSdf.format(time) + shortFormat -> otherYearSdf.format(time) + else -> otherYearCompleteSdf.format(time) + } + } + + companion object { + + private fun isSameDate(dateOne: Date, dateTwo: Date, tz: TimeZone): Boolean { + val calendarOne = Calendar.getInstance(tz).apply { time = dateOne } + val calendarTwo = Calendar.getInstance(tz).apply { time = dateTwo } + + return calendarOne.get(Calendar.YEAR) == calendarTwo.get(Calendar.YEAR) && + calendarOne.get(Calendar.MONTH) == calendarTwo.get(Calendar.MONTH) && + calendarOne.get(Calendar.DAY_OF_MONTH) == calendarTwo.get(Calendar.DAY_OF_MONTH) + } + + private fun isSameYear(dateOne: Date, dateTwo: Date, timeZone1: TimeZone): Boolean { + val calendarOne = Calendar.getInstance(timeZone1).apply { time = dateOne } + val calendarTwo = Calendar.getInstance(timeZone1).apply { time = dateTwo } + + return calendarOne.get(Calendar.YEAR) == calendarTwo.get(Calendar.YEAR) + } + } +} 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 60ac73f4..0752c4e5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt @@ -34,20 +34,16 @@ import com.keylesspalace.tusky.viewdata.PollViewData import com.keylesspalace.tusky.viewdata.buildDescription import com.keylesspalace.tusky.viewdata.calculatePercent import java.text.NumberFormat -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale import kotlin.math.min class StatusViewHelper(private val itemView: View) { + private val absoluteTimeFormatter = AbsoluteTimeFormatter() + interface MediaPreviewListener { fun onViewMedia(v: View?, idx: Int) fun onContentHiddenChange(isShowing: Boolean) } - private val shortSdf = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) - private val longSdf = SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault()) - fun setMediasPreview( statusDisplayOptions: StatusDisplayOptions, attachments: List, @@ -295,7 +291,7 @@ class StatusViewHelper(private val itemView: View) { context.getString(R.string.poll_info_closed) } else { if (useAbsoluteTime) { - context.getString(R.string.poll_info_time_absolute, getAbsoluteTime(poll.expiresAt)) + context.getString(R.string.poll_info_time_absolute, absoluteTimeFormatter.format(poll.expiresAt, false)) } else { TimestampUtils.formatPollDuration(context, poll.expiresAt!!.time, timestamp) } @@ -330,18 +326,6 @@ class StatusViewHelper(private val itemView: View) { } } - fun getAbsoluteTime(time: Date?): String { - return if (time != null) { - if (android.text.format.DateUtils.isToday(time.time)) { - shortSdf.format(time) - } else { - longSdf.format(time) - } - } else { - "??:??:??" - } - } - companion object { val COLLAPSE_INPUT_FILTER = arrayOf(SmartLengthInputFilter) val NO_INPUT_FILTER = arrayOfNulls(0) diff --git a/app/src/test/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatterTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatterTest.kt new file mode 100644 index 00000000..57f3bed4 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatterTest.kt @@ -0,0 +1,46 @@ +package com.keylesspalace.tusky.util + +import org.junit.Assert.assertEquals +import org.junit.Test +import java.time.Instant +import java.util.Date +import java.util.TimeZone + +class AbsoluteTimeFormatterTest { + + private val formatter = AbsoluteTimeFormatter(TimeZone.getTimeZone("UTC")) + private val now = Date.from(Instant.parse("2022-04-11T00:00:00.00Z")) + + @Test + fun `null handling`() { + assertEquals("??", formatter.format(null, true, now)) + assertEquals("??", formatter.format(null, false, now)) + } + + @Test + fun `same day formatting`() { + val tenTen = Date.from(Instant.parse("2022-04-11T10:10:00.00Z")) + assertEquals("10:10", formatter.format(tenTen, true, now)) + assertEquals("10:10", formatter.format(tenTen, false, now)) + } + + @Test + fun `same year formatting`() { + val nextDay = Date.from(Instant.parse("2022-04-12T00:10:00.00Z")) + assertEquals("04-12 00:10", formatter.format(nextDay, true, now)) + assertEquals("04-12 00:10", formatter.format(nextDay, false, now)) + val endOfYear = Date.from(Instant.parse("2022-12-31T23:59:00.00Z")) + assertEquals("12-31 23:59", formatter.format(endOfYear, true, now)) + assertEquals("12-31 23:59", formatter.format(endOfYear, false, now)) + } + + @Test + fun `other year formatting`() { + val firstDayNextYear = Date.from(Instant.parse("2023-01-01T00:00:00.00Z")) + assertEquals("2023-01-01", formatter.format(firstDayNextYear, true, now)) + assertEquals("2023-01-01 00:00", formatter.format(firstDayNextYear, false, now)) + val inTenYears = Date.from(Instant.parse("2032-04-11T10:10:00.00Z")) + assertEquals("2032-04-11", formatter.format(inTenYears, true, now)) + assertEquals("2032-04-11 10:10", formatter.format(inTenYears, false, now)) + } +} From dff039e123166c69e16cccb27afe429682289052 Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Tue, 19 Apr 2022 11:10:13 +0200 Subject: [PATCH 011/104] Add support for post edit notifications (#2431) * Add support for post edit notifications * Update notification icon --- .../34.json | 815 ++++++++++++++++++ .../tusky/adapter/NotificationsAdapter.java | 42 +- .../notifications/NotificationHelper.java | 8 + .../NotificationPreferencesFragment.kt | 11 + .../keylesspalace/tusky/db/AccountEntity.kt | 1 + .../keylesspalace/tusky/db/AppDatabase.java | 9 +- .../com/keylesspalace/tusky/di/AppModule.kt | 2 +- .../tusky/entity/Notification.kt | 3 +- .../tusky/fragment/NotificationsFragment.java | 2 + .../tusky/settings/SettingsConstants.kt | 1 + app/src/main/res/drawable/ic_edit_24dp.xml | 10 + app/src/main/res/values/strings.xml | 4 + 12 files changed, 885 insertions(+), 23 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/34.json create mode 100644 app/src/main/res/drawable/ic_edit_24dp.xml diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/34.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/34.json new file mode 100644 index 00000000..c1354690 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/34.json @@ -0,0 +1,815 @@ +{ + "formatVersion": 1, + "database": { + "version": 34, + "identityHash": "7f766d68ab5d72a7988cd81c183e9a9d", + "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, `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)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "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 + } + ], + "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, 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 + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT 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, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": 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 + } + ], + "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, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_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": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.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, '7f766d68ab5d72a7988cd81c183e9a9d')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index c1f78b46..0ac029b0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -32,6 +32,8 @@ import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.ColorRes; +import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; @@ -201,7 +203,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { holder.setUsername(status.getAccount().getUsername()); holder.setCreatedAt(status.getCreatedAt()); - if (concreteNotificaton.getType() == Notification.Type.STATUS) { + if (concreteNotificaton.getType() == Notification.Type.STATUS || + concreteNotificaton.getType() == Notification.Type.UPDATE) { holder.setAvatar(status.getAccount().getAvatar(), status.getAccount().getBot()); } else { holder.setAvatars(status.getAccount().getAvatar(), @@ -280,7 +283,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } case STATUS: case FAVOURITE: - case REBLOG: { + case REBLOG: + case UPDATE: { return VIEW_TYPE_STATUS_NOTIFICATION; } case FOLLOW: @@ -474,6 +478,14 @@ public class NotificationsAdapter extends RecyclerView.Adapter { } } + Drawable getIconWithColor(Context context, @DrawableRes int drawable, @ColorRes int color) { + Drawable icon = ContextCompat.getDrawable(context, drawable); + if (icon != null) { + icon.setColorFilter(ContextCompat.getColor(context, color), PorterDuff.Mode.SRC_ATOP); + } + return icon; + } + void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener) { this.statusViewData = notificationViewData.getStatusViewData(); @@ -486,35 +498,25 @@ public class NotificationsAdapter extends RecyclerView.Adapter { switch (type) { default: case FAVOURITE: { - icon = ContextCompat.getDrawable(context, R.drawable.ic_star_24dp); - if (icon != null) { - icon.setColorFilter(ContextCompat.getColor(context, - R.color.tusky_orange), PorterDuff.Mode.SRC_ATOP); - } - + icon = getIconWithColor(context, R.drawable.ic_star_24dp, R.color.tusky_orange); format = context.getString(R.string.notification_favourite_format); break; } case REBLOG: { - icon = ContextCompat.getDrawable(context, R.drawable.ic_repeat_24dp); - if (icon != null) { - icon.setColorFilter(ContextCompat.getColor(context, - R.color.tusky_blue), PorterDuff.Mode.SRC_ATOP); - } - + icon = getIconWithColor(context, R.drawable.ic_repeat_24dp, R.color.tusky_blue); format = context.getString(R.string.notification_reblog_format); break; } case STATUS: { - icon = ContextCompat.getDrawable(context, R.drawable.ic_home_24dp); - if (icon != null) { - icon.setColorFilter(ContextCompat.getColor(context, - R.color.tusky_blue), PorterDuff.Mode.SRC_ATOP); - } - + icon = getIconWithColor(context, R.drawable.ic_home_24dp, R.color.tusky_blue); format = context.getString(R.string.notification_subscription_format); break; } + case UPDATE: { + icon = getIconWithColor(context, R.drawable.ic_edit_24dp, R.color.tusky_blue); + format = context.getString(R.string.notification_update_format); + break; + } } message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); String wholeMessage = String.format(format, displayName); 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 83682ab2..79586897 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 @@ -118,6 +118,7 @@ public class NotificationHelper { public static final String CHANNEL_POLL = "CHANNEL_POLL"; public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS"; public static final String CHANNEL_SIGN_UP = "CHANNEL_SIGN_UP"; + public static final String CHANNEL_UPDATES = "CHANNEL_UPDATES"; /** * WorkManager Tag @@ -395,6 +396,7 @@ public class NotificationHelper { CHANNEL_POLL + account.getIdentifier(), CHANNEL_SUBSCRIPTIONS + account.getIdentifier(), CHANNEL_SIGN_UP + account.getIdentifier(), + CHANNEL_UPDATES + account.getIdentifier(), }; int[] channelNames = { R.string.notification_mention_name, @@ -405,6 +407,7 @@ public class NotificationHelper { R.string.notification_poll_name, R.string.notification_subscription_name, R.string.notification_sign_up_name, + R.string.notification_update_name, }; int[] channelDescriptions = { R.string.notification_mention_descriptions, @@ -415,6 +418,7 @@ public class NotificationHelper { R.string.notification_poll_description, R.string.notification_subscription_description, R.string.notification_sign_up_description, + R.string.notification_update_description, }; List channels = new ArrayList<>(6); @@ -567,6 +571,8 @@ public class NotificationHelper { return account.getNotificationsPolls(); case SIGN_UP: return account.getNotificationsSignUps(); + case UPDATE: + return account.getNotificationsUpdates(); default: return false; } @@ -674,6 +680,8 @@ public class NotificationHelper { } case SIGN_UP: return String.format(context.getString(R.string.notification_sign_up_format), accountName); + case UPDATE: + return String.format(context.getString(R.string.notification_update_format), accountName); } return null; } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt index 82ee0a38..6fdc1e8a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt @@ -133,6 +133,17 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Injectable { true } } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_updates) + key = PrefKeys.NOTIFICATION_FILTER_UPDATES + isIconSpaceReserved = false + isChecked = activeAccount.notificationsUpdates + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsUpdates = newValue as Boolean } + true + } + } } preferenceCategory(R.string.pref_title_notification_alerts) { category -> diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt index 5da91e20..400eb073 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt @@ -51,6 +51,7 @@ data class AccountEntity( var notificationsPolls: Boolean = true, var notificationsSubscriptions: Boolean = true, var notificationsSignUps: Boolean = true, + var notificationsUpdates: Boolean = true, var notificationSound: Boolean = true, var notificationVibration: Boolean = true, var notificationLight: Boolean = true, diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index c541958a..293db65e 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 = 33) + }, version = 34) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -527,4 +527,11 @@ public abstract class AppDatabase extends RoomDatabase { "PRIMARY KEY(`id`, `accountId`))"); } }; + + public static final Migration MIGRATION_33_34 = new Migration(33, 34) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsUpdates` INTEGER NOT NULL DEFAULT 1"); + } + }; } 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 7f0fbd01..c92d52ef 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -63,7 +63,7 @@ class AppModule { AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")), AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29, AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32, - AppDatabase.MIGRATION_32_33 + AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34 ) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt index ddcf5e61..f6e38150 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt @@ -39,6 +39,7 @@ data class Notification( POLL("poll"), STATUS("status"), SIGN_UP("admin.sign_up"), + UPDATE("update"), ; companion object { @@ -51,7 +52,7 @@ data class Notification( } return UNKNOWN } - val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS, SIGN_UP) + val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS, SIGN_UP, UPDATE) } override fun toString(): String { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index f267f29e..94ee496e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -709,6 +709,8 @@ public class NotificationsFragment extends SFragment implements return getString(R.string.notification_subscription_name); case SIGN_UP: return getString(R.string.notification_sign_up_name); + case UPDATE: + return getString(R.string.notification_update_name); default: return "Unknown"; } diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt index c728e1f6..6540601a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt @@ -60,6 +60,7 @@ object PrefKeys { const val NOTIFICATIONS_FILTER_FOLLOWS = "notificationFilterFollows" const val NOTIFICATION_FILTER_SUBSCRIPTIONS = "notificationFilterSubscriptions" const val NOTIFICATION_FILTER_SIGN_UPS = "notificationFilterSignUps" + const val NOTIFICATION_FILTER_UPDATES = "notificationFilterUpdates" const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies" const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts" diff --git a/app/src/main/res/drawable/ic_edit_24dp.xml b/app/src/main/res/drawable/ic_edit_24dp.xml new file mode 100644 index 00000000..2844bafe --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_24dp.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e6ce2338..e007c50e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,6 +64,7 @@ %s requested to follow you %s signed up %s just posted + %s edited their post Report @%s Additional comments? @@ -230,6 +231,7 @@ polls have ended somebody I\'m subscribed to published a new post somebody signed up + a post I\'ve interacted with is edited Appearance App Theme Timelines @@ -299,6 +301,8 @@ Notifications when somebody you\'re subscribed to published a new post Sign ups Notifications about new users + Post edits + Notifications when posts you\'ve interacted with are edited %s mentioned you %1$s, %2$s, %3$s and %4$d others From b5d8b730443698273219d92282c025e9360c4dbb Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 19 Apr 2022 11:10:23 +0200 Subject: [PATCH 012/104] fix "account moved" on profiles not being clickable (#2438) --- .../components/account/AccountActivity.kt | 9 -- app/src/main/res/drawable/ic_briefcase.xml | 2 +- app/src/main/res/layout/activity_account.xml | 112 ++++++++++-------- 3 files changed, 61 insertions(+), 62 deletions(-) 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 114a6cd0..ddecd20c 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 @@ -20,8 +20,6 @@ import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.graphics.Color -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter import android.os.Bundle import android.text.Editable import android.view.Menu @@ -499,13 +497,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI loadAvatar(movedAccount.avatar, binding.accountMovedAvatar, avatarRadius, animateAvatar) binding.accountMovedText.text = getString(R.string.account_moved_description, movedAccount.name) - - // this is necessary because API 19 can't handle vector compound drawables - val movedIcon = ContextCompat.getDrawable(this, R.drawable.ic_briefcase)?.mutate() - val textColor = ThemeUtils.getColor(this, android.R.attr.textColorTertiary) - movedIcon?.colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN) - - binding.accountMovedText.setCompoundDrawablesRelativeWithIntrinsicBounds(movedIcon, null, null, null) } } diff --git a/app/src/main/res/drawable/ic_briefcase.xml b/app/src/main/res/drawable/ic_briefcase.xml index eeb80619..6df5b88b 100644 --- a/app/src/main/res/drawable/ic_briefcase.xml +++ b/app/src/main/res/drawable/ic_briefcase.xml @@ -4,6 +4,6 @@ android:viewportHeight="24" android:viewportWidth="24"> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index 57064412..89a56d9e 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -248,63 +248,71 @@ app:layout_constraintTop_toBottomOf="@id/accountFieldList" tools:visibility="visible" /> - - - + tools:visibility="visible"> - + - + - + + + + + + app:layout_constraintTop_toBottomOf="@id/accountMovedView"> + app:layout_constraintTop_toBottomOf="@id/accountMovedView"> + app:layout_constraintTop_toBottomOf="@id/accountMovedView"> Date: Fri, 15 Apr 2022 20:40:27 +0000 Subject: [PATCH 013/104] Translated using Weblate (French) Currently translated at 100.0% (16 of 16 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/fr/ --- fastlane/metadata/android/fr/changelogs/89.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 fastlane/metadata/android/fr/changelogs/89.txt diff --git a/fastlane/metadata/android/fr/changelogs/89.txt b/fastlane/metadata/android/fr/changelogs/89.txt new file mode 100644 index 00000000..9c8ae3b9 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- L'option « Ouvrir comme… » disponible quand plusieurs comptes sont connectés est maintenant aussi accessible depuis le menu sur les profils +- L'identification se fait maintenant par une WebView dans l'application +- Android 12 est pris en charge +- La nouvelle API Mastodon de configuration d'instance est prise en charge +- et beaucoup d'autres petites corrections et améliorations From d2bfceae7bf232a07157a9f60571d7e5ae89225a Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 21 Apr 2022 18:46:21 +0200 Subject: [PATCH 014/104] refactor compose & announcements to coroutines (#2446) * refactor compose & announcements to coroutines * fix code formatting * add javadoc to InstanceInfoRepository * fix comments in ImageDownsizer * remove unused Either extensions * add explicit return type for InstanceInfoRepository.getEmojis * make ComposeViewModel.pickMedia return Result * cleanup code in ImageDownsizer --- app/build.gradle | 1 - .../com/keylesspalace/tusky/MainActivity.kt | 24 +- .../announcements/AnnouncementsViewModel.kt | 264 ++++++++---------- .../components/compose/ComposeActivity.kt | 37 +-- .../components/compose/ComposeViewModel.kt | 205 ++++---------- .../components/compose/DownsizeImageTask.java | 154 ---------- .../components/compose/ImageDownsizer.kt | 101 +++++++ .../tusky/components/compose/MediaUploader.kt | 204 +++++++------- .../compose/dialog/CaptionDialog.kt | 15 +- .../components/instanceinfo/InstanceInfo.kt | 26 ++ .../instanceinfo/InstanceInfoRepository.kt | 104 +++++++ .../com/keylesspalace/tusky/db/InstanceDao.kt | 14 +- .../keylesspalace/tusky/db/InstanceEntity.kt | 19 +- .../tusky/network/MastodonApi.kt | 26 +- .../tusky/ComposeActivityTest.kt | 30 +- 15 files changed, 596 insertions(+), 628 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/DownsizeImageTask.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/compose/ImageDownsizer.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt diff --git a/app/build.gradle b/app/build.gradle index 6ed56f42..02d7907d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -124,7 +124,6 @@ dependencies { implementation "androidx.work:work-runtime:2.7.1" implementation "androidx.room:room-ktx:$roomVersion" implementation "androidx.room:room-paging:$roomVersion" - implementation "androidx.room:room-rxjava3:$roomVersion" kapt "androidx.room:room-compiler:$roomVersion" implementation 'androidx.core:core-splashscreen:1.0.0-beta02' diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index a934ff9a..3f559399 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -779,18 +779,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } private fun fetchAnnouncements() { - mastodonApi.listAnnouncements(false) - .observeOn(AndroidSchedulers.mainThread()) - .autoDispose(this, Lifecycle.Event.ON_DESTROY) - .subscribe( - { announcements -> - unreadAnnouncementsCount = announcements.count { !it.read } - updateAnnouncementsBadge() - }, - { - Log.w(TAG, "Failed to fetch announcements.", it) - } - ) + lifecycleScope.launch { + mastodonApi.listAnnouncements(false) + .fold( + { announcements -> + unreadAnnouncementsCount = announcements.count { !it.read } + updateAnnouncementsBadge() + }, + { throwable -> + Log.w(TAG, "Failed to fetch announcements.", throwable) + } + ) + } } private fun updateAnnouncementsBadge() { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt index 10dc303f..0934c48f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt @@ -18,32 +18,26 @@ package com.keylesspalace.tusky.components.announcements import android.util.Log import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import com.keylesspalace.tusky.appstore.AnnouncementReadEvent import com.keylesspalace.tusky.appstore.EventHub -import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH -import com.keylesspalace.tusky.db.AccountManager -import com.keylesspalace.tusky.db.AppDatabase -import com.keylesspalace.tusky.db.InstanceEntity +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository import com.keylesspalace.tusky.entity.Announcement import com.keylesspalace.tusky.entity.Emoji -import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.Error import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Resource -import com.keylesspalace.tusky.util.RxAwareViewModel import com.keylesspalace.tusky.util.Success -import io.reactivex.rxjava3.core.Single -import kotlinx.coroutines.rx3.rxSingle +import kotlinx.coroutines.launch import javax.inject.Inject class AnnouncementsViewModel @Inject constructor( - accountManager: AccountManager, - private val appDatabase: AppDatabase, + private val instanceInfoRepo: InstanceInfoRepository, private val mastodonApi: MastodonApi, private val eventHub: EventHub -) : RxAwareViewModel() { +) : ViewModel() { private val announcementsMutable = MutableLiveData>>() val announcements: LiveData>> = announcementsMutable @@ -52,156 +46,130 @@ class AnnouncementsViewModel @Inject constructor( val emojis: LiveData> = emojisMutable init { - Single.zip( - mastodonApi.getCustomEmojis(), - appDatabase.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) - .map> { Either.Left(it) } - .onErrorResumeNext { - rxSingle { - mastodonApi.getInstance().getOrThrow() - }.map { Either.Right(it) } - } - ) { emojis, either -> - either.asLeftOrNull()?.copy(emojiList = emojis) - ?: InstanceEntity( - accountManager.activeAccount?.domain!!, - emojis, - either.asRight().configuration?.statuses?.maxCharacters ?: either.asRight().maxTootChars, - either.asRight().configuration?.polls?.maxOptions ?: either.asRight().pollConfiguration?.maxOptions, - either.asRight().configuration?.polls?.maxCharactersPerOption ?: either.asRight().pollConfiguration?.maxOptionChars, - either.asRight().configuration?.polls?.minExpiration ?: either.asRight().pollConfiguration?.minExpiration, - either.asRight().configuration?.polls?.maxExpiration ?: either.asRight().pollConfiguration?.maxExpiration, - either.asRight().configuration?.statuses?.charactersReservedPerUrl ?: DEFAULT_MAXIMUM_URL_LENGTH, - either.asRight().version - ) + viewModelScope.launch { + emojisMutable.postValue(instanceInfoRepo.getEmojis()) } - .doOnSuccess { - appDatabase.instanceDao().insertOrReplace(it) - } - .subscribe( - { - emojisMutable.postValue(it.emojiList.orEmpty()) - }, - { - Log.w(TAG, "Failed to get custom emojis.", it) - } - ) - .autoDispose() } fun load() { - announcementsMutable.postValue(Loading()) - mastodonApi.listAnnouncements() - .subscribe( - { - announcementsMutable.postValue(Success(it)) - it.filter { announcement -> !announcement.read } - .forEach { announcement -> - mastodonApi.dismissAnnouncement(announcement.id) - .subscribe( - { - eventHub.dispatch(AnnouncementReadEvent(announcement.id)) - }, - { throwable -> - Log.d(TAG, "Failed to mark announcement as read.", throwable) - } - ) - .autoDispose() - } - }, - { - announcementsMutable.postValue(Error(cause = it)) - } - ) - .autoDispose() + viewModelScope.launch { + announcementsMutable.postValue(Loading()) + mastodonApi.listAnnouncements() + .fold( + { + announcementsMutable.postValue(Success(it)) + it.filter { announcement -> !announcement.read } + .forEach { announcement -> + mastodonApi.dismissAnnouncement(announcement.id) + .fold( + { + eventHub.dispatch(AnnouncementReadEvent(announcement.id)) + }, + { throwable -> + Log.d( + TAG, + "Failed to mark announcement as read.", + throwable + ) + } + ) + } + }, + { + announcementsMutable.postValue(Error(cause = it)) + } + ) + } } fun addReaction(announcementId: String, name: String) { - mastodonApi.addAnnouncementReaction(announcementId, name) - .subscribe( - { - announcementsMutable.postValue( - Success( - announcements.value!!.data!!.map { announcement -> - if (announcement.id == announcementId) { - announcement.copy( - reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) { - announcement.reactions.map { reaction -> + viewModelScope.launch { + mastodonApi.addAnnouncementReaction(announcementId, name) + .fold( + { + announcementsMutable.postValue( + Success( + announcements.value!!.data!!.map { announcement -> + if (announcement.id == announcementId) { + announcement.copy( + reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) { + announcement.reactions.map { reaction -> + if (reaction.name == name) { + reaction.copy( + count = reaction.count + 1, + me = true + ) + } else { + reaction + } + } + } else { + listOf( + *announcement.reactions.toTypedArray(), + emojis.value!!.find { emoji -> emoji.shortcode == name } + !!.run { + Announcement.Reaction( + name, + 1, + true, + url, + staticUrl + ) + } + ) + } + ) + } else { + announcement + } + } + ) + ) + }, + { + Log.w(TAG, "Failed to add reaction to the announcement.", it) + } + ) + } + } + + fun removeReaction(announcementId: String, name: String) { + viewModelScope.launch { + mastodonApi.removeAnnouncementReaction(announcementId, name) + .fold( + { + announcementsMutable.postValue( + Success( + announcements.value!!.data!!.map { announcement -> + if (announcement.id == announcementId) { + announcement.copy( + reactions = announcement.reactions.mapNotNull { reaction -> if (reaction.name == name) { - reaction.copy( - count = reaction.count + 1, - me = true - ) + if (reaction.count > 1) { + reaction.copy( + count = reaction.count - 1, + me = false + ) + } else { + null + } } else { reaction } } - } else { - listOf( - *announcement.reactions.toTypedArray(), - emojis.value!!.find { emoji -> emoji.shortcode == name } - !!.run { - Announcement.Reaction( - name, - 1, - true, - url, - staticUrl - ) - } - ) - } - ) - } else { - announcement + ) + } else { + announcement + } } - } + ) ) - ) - }, - { - Log.w(TAG, "Failed to add reaction to the announcement.", it) - } - ) - .autoDispose() - } - - fun removeReaction(announcementId: String, name: String) { - mastodonApi.removeAnnouncementReaction(announcementId, name) - .subscribe( - { - announcementsMutable.postValue( - Success( - announcements.value!!.data!!.map { announcement -> - if (announcement.id == announcementId) { - announcement.copy( - reactions = announcement.reactions.mapNotNull { reaction -> - if (reaction.name == name) { - if (reaction.count > 1) { - reaction.copy( - count = reaction.count - 1, - me = false - ) - } else { - null - } - } else { - reaction - } - } - ) - } else { - announcement - } - } - ) - ) - }, - { - Log.w(TAG, "Failed to remove reaction from the announcement.", it) - } - ) - .autoDispose() + }, + { + Log.w(TAG, "Failed to remove reaction from the announcement.", it) + } + ) + } } companion object { 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 ed590cd1..b7a65a7f 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 @@ -51,6 +51,7 @@ import androidx.core.view.ContentInfoCompat import androidx.core.view.OnReceiveContentListener import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.transition.TransitionManager @@ -65,6 +66,7 @@ import com.keylesspalace.tusky.components.compose.dialog.makeCaptionDialog import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository import com.keylesspalace.tusky.databinding.ActivityComposeBinding import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.DraftAttachment @@ -93,6 +95,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 kotlinx.parcelize.Parcelize import java.io.File import java.io.IOException @@ -123,8 +126,8 @@ class ComposeActivity : private var photoUploadUri: Uri? = null @VisibleForTesting - var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT - var charactersReservedPerUrl = DEFAULT_MAXIMUM_URL_LENGTH + var maximumTootCharacters = InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT + var charactersReservedPerUrl = InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL private val viewModel: ComposeViewModel by viewModels { viewModelFactory } @@ -328,7 +331,7 @@ class ComposeActivity : private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter) { withLifecycleContext { - viewModel.instanceParams.observe { instanceData -> + viewModel.instanceInfo.observe { instanceData -> maximumTootCharacters = instanceData.maxChars charactersReservedPerUrl = instanceData.charactersReservedPerUrl updateVisibleCharactersLeft() @@ -666,7 +669,7 @@ class ComposeActivity : private fun openPollDialog() { addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED - val instanceParams = viewModel.instanceParams.value!! + val instanceParams = viewModel.instanceInfo.value!! showAddPollDialog( this, viewModel.poll.value, instanceParams.pollMaxOptions, instanceParams.pollMaxLength, instanceParams.pollMinDuration, instanceParams.pollMaxDuration, @@ -866,25 +869,15 @@ class ComposeActivity : } private fun pickMedia(uri: Uri) { - withLifecycleContext { - viewModel.pickMedia(uri).observe { exceptionOrItem -> - exceptionOrItem.asLeftOrNull()?.let { - val errorId = when (it) { - 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 - } - } - displayTransientError(errorId) + 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 } + displayTransientError(errorId) } } } 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 08df6dc9..fce3d0bd 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 @@ -20,14 +20,14 @@ import android.util.Log import androidx.core.net.toUri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.Observer +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia import com.keylesspalace.tusky.components.drafts.DraftHelper +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository import com.keylesspalace.tusky.components.search.SearchType import com.keylesspalace.tusky.db.AccountManager -import com.keylesspalace.tusky.db.AppDatabase -import com.keylesspalace.tusky.db.InstanceEntity import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.NewPoll @@ -35,9 +35,6 @@ 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.Either -import com.keylesspalace.tusky.util.RxAwareViewModel -import com.keylesspalace.tusky.util.VersionUtils import com.keylesspalace.tusky.util.combineLiveData import com.keylesspalace.tusky.util.filter import com.keylesspalace.tusky.util.map @@ -45,10 +42,12 @@ import com.keylesspalace.tusky.util.randomAlphanumericString import com.keylesspalace.tusky.util.toLiveData import com.keylesspalace.tusky.util.withoutFirstWhich import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.disposables.Disposable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.rxSingle +import kotlinx.coroutines.withContext import java.util.Locale import javax.inject.Inject @@ -58,8 +57,8 @@ class ComposeViewModel @Inject constructor( private val mediaUploader: MediaUploader, private val serviceClient: ServiceClient, private val draftHelper: DraftHelper, - private val db: AppDatabase -) : RxAwareViewModel() { + private val instanceInfoRepo: InstanceInfoRepository +) : ViewModel() { private var replyingStatusAuthor: String? = null private var replyingStatusContent: String? = null @@ -73,19 +72,8 @@ class ComposeViewModel @Inject constructor( private var contentWarningStateChanged: Boolean = false private var modifiedInitialState: Boolean = false - private val instance: MutableLiveData = MutableLiveData(null) + val instanceInfo: MutableLiveData = MutableLiveData() - val instanceParams: LiveData = instance.map { instance -> - ComposeInstanceParams( - maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT, - pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT, - pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH, - pollMinDuration = instance?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION, - pollMaxDuration = instance?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION, - charactersReservedPerUrl = instance?.charactersReservedPerUrl ?: DEFAULT_MAXIMUM_URL_LENGTH, - supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false - ) - } val emoji: MutableLiveData?> = MutableLiveData() val markMediaAsSensitive = mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false) @@ -99,75 +87,35 @@ class ComposeViewModel @Inject constructor( val media = mutableLiveData>(listOf()) val uploadError = MutableLiveData() - private val mediaToDisposable = mutableMapOf() + private val mediaToJob = mutableMapOf() private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty() init { - - Single.zip( - api.getCustomEmojis(), - rxSingle { - api.getInstance().getOrThrow() - } - ) { emojis, instance -> - InstanceEntity( - instance = accountManager.activeAccount?.domain!!, - emojiList = emojis, - maximumTootCharacters = instance.configuration?.statuses?.maxCharacters ?: instance.maxTootChars, - maxPollOptions = instance.configuration?.polls?.maxOptions ?: instance.pollConfiguration?.maxOptions, - maxPollOptionLength = instance.configuration?.polls?.maxCharactersPerOption ?: instance.pollConfiguration?.maxOptionChars, - minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration, - maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration, - charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl, - version = instance.version - ) + viewModelScope.launch { + emoji.postValue(instanceInfoRepo.getEmojis()) + } + viewModelScope.launch { + instanceInfo.postValue(instanceInfoRepo.getInstanceInfo()) } - .doOnSuccess { - db.instanceDao().insertOrReplace(it) - } - .onErrorResumeNext { - db.instanceDao().loadMetadataForInstance(accountManager.activeAccount?.domain!!) - } - .subscribe( - { instanceEntity -> - emoji.postValue(instanceEntity.emojiList) - instance.postValue(instanceEntity) - }, - { throwable -> - // this can happen on network error when no cached data is available - Log.w(TAG, "error loading instance data", throwable) - } - ) - .autoDispose() } - fun pickMedia(uri: Uri, description: String? = null): LiveData> { - // We are not calling .toLiveData() here because we don't want to stop the process when - // the Activity goes away temporarily (like on screen rotation). - val liveData = MutableLiveData>() - mediaUploader.prepareMedia(uri) - .map { (type, uri, size) -> - val mediaItems = media.value!! - if (type != QueuedMedia.Type.IMAGE && - mediaItems.isNotEmpty() && - mediaItems[0].type == QueuedMedia.Type.IMAGE - ) { - throw VideoOrImageException() - } else { - addMediaToQueue(type, uri, size, description) - } + suspend fun pickMedia(mediaUri: Uri, description: String? = null): Result = withContext(Dispatchers.IO) { + try { + val (type, uri, size) = mediaUploader.prepareMedia(mediaUri) + val mediaItems = media.value!! + if (type != QueuedMedia.Type.IMAGE && + mediaItems.isNotEmpty() && + mediaItems[0].type == QueuedMedia.Type.IMAGE + ) { + Result.failure(VideoOrImageException()) + } else { + val queuedMedia = addMediaToQueue(type, uri, size, description) + Result.success(queuedMedia) } - .subscribe( - { queuedMedia -> - liveData.postValue(Either.Right(queuedMedia)) - }, - { error -> - liveData.postValue(Either.Left(error)) - } - ) - .autoDispose() - return liveData + } catch (e: Exception) { + Result.failure(e) + } } private fun addMediaToQueue( @@ -183,13 +131,17 @@ class ComposeViewModel @Inject constructor( mediaSize = mediaSize, description = description ) - media.value = media.value!! + mediaItem - mediaToDisposable[mediaItem.localId] = mediaUploader - .uploadMedia(mediaItem) - .subscribe( - { event -> + media.postValue(media.value!! + mediaItem) + mediaToJob[mediaItem.localId] = viewModelScope.launch { + mediaUploader + .uploadMedia(mediaItem) + .catch { error -> + media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList()) + uploadError.postValue(error) + } + .collect { event -> val item = media.value?.find { it.localId == mediaItem.localId } - ?: return@subscribe + ?: return@collect val newMediaItem = when (event) { is UploadEvent.ProgressEvent -> item.copy(uploadPercent = event.percentage) @@ -207,12 +159,8 @@ class ComposeViewModel @Inject constructor( } ) } - }, - { error -> - media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList()) - uploadError.postValue(error) } - ) + } return mediaItem } @@ -222,7 +170,7 @@ class ComposeViewModel @Inject constructor( } fun removeMediaFromQueue(item: QueuedMedia) { - mediaToDisposable[item.localId]?.dispose() + mediaToJob[item.localId]?.cancel() media.value = media.value!!.withoutFirstWhich { it.localId == item.localId } } @@ -337,35 +285,24 @@ class ComposeViewModel @Inject constructor( return combineLiveData(deletionObservable, sendObservable) { _, _ -> } } - fun updateDescription(localId: Long, description: String): LiveData { + suspend fun updateDescription(localId: Long, description: String): Boolean { val newList = media.value!!.toMutableList() val index = newList.indexOfFirst { it.localId == localId } if (index != -1) { newList[index] = newList[index].copy(description = description) } media.value = newList - val completedCaptioningLiveData = MutableLiveData() - media.observeForever(object : Observer> { - override fun onChanged(mediaItems: List) { - val updatedItem = mediaItems.find { it.localId == localId } - if (updatedItem == null) { - media.removeObserver(this) - } else if (updatedItem.id != null) { - api.updateMedia(updatedItem.id, description) - .subscribe( - { - completedCaptioningLiveData.postValue(true) - }, - { - completedCaptioningLiveData.postValue(false) - } - ) - .autoDispose() - media.removeObserver(this) - } - } - }) - return completedCaptioningLiveData + val updatedItem = newList.find { it.localId == localId } + if (updatedItem?.id != null) { + return api.updateMedia(updatedItem.id, description) + .fold({ + true + }, { throwable -> + Log.w(TAG, "failed to update media", throwable) + false + }) + } + return true } fun searchAutocompleteSuggestions(token: String): List { @@ -447,7 +384,11 @@ class ComposeViewModel @Inject constructor( val draftAttachments = composeOptions?.draftAttachments if (draftAttachments != null) { // when coming from DraftActivity - draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) } + draftAttachments.forEach { attachment -> + viewModelScope.launch { + pickMedia(attachment.uri, attachment.description) + } + } } else composeOptions?.mediaAttachments?.forEach { a -> // when coming from redraft or ScheduledTootActivity val mediaType = when (a.type) { @@ -498,13 +439,6 @@ class ComposeViewModel @Inject constructor( scheduledAt.value = newScheduledAt } - override fun onCleared() { - for (uploadDisposable in mediaToDisposable.values) { - uploadDisposable.dispose() - } - super.onCleared() - } - private companion object { const val TAG = "ComposeViewModel" } @@ -512,25 +446,6 @@ class ComposeViewModel @Inject constructor( fun mutableLiveData(default: T) = MutableLiveData().apply { value = default } -const val DEFAULT_CHARACTER_LIMIT = 500 -private const val DEFAULT_MAX_OPTION_COUNT = 4 -private const val DEFAULT_MAX_OPTION_LENGTH = 50 -private const val DEFAULT_MIN_POLL_DURATION = 300 -private const val DEFAULT_MAX_POLL_DURATION = 604800 - -// Mastodon only counts URLs as this long in terms of status character limits -const val DEFAULT_MAXIMUM_URL_LENGTH = 23 - -data class ComposeInstanceParams( - val maxChars: Int, - val pollMaxOptions: Int, - val pollMaxLength: Int, - val pollMinDuration: Int, - val pollMaxDuration: Int, - val charactersReservedPerUrl: Int, - val supportsScheduled: Boolean -) - /** * 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/DownsizeImageTask.java b/app/src/main/java/com/keylesspalace/tusky/components/compose/DownsizeImageTask.java deleted file mode 100644 index 880a4167..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/DownsizeImageTask.java +++ /dev/null @@ -1,154 +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.components.compose; - -import android.content.ContentResolver; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.os.AsyncTask; - -import com.keylesspalace.tusky.util.IOUtils; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.io.OutputStream; - -import static com.keylesspalace.tusky.util.MediaUtilsKt.calculateInSampleSize; -import static com.keylesspalace.tusky.util.MediaUtilsKt.getImageOrientation; -import static com.keylesspalace.tusky.util.MediaUtilsKt.reorientBitmap; - -/** - * Reduces the file size of images to fit under a given limit by resizing them, maintaining both - * aspect ratio and orientation. - */ -public class DownsizeImageTask extends AsyncTask { - private int sizeLimit; - private ContentResolver contentResolver; - private Listener listener; - private File tempFile; - - /** - * @param sizeLimit the maximum number of bytes each image can take - * @param contentResolver to resolve the specified images' URIs - * @param tempFile the file where the result will be stored - * @param listener to whom the results are given - */ - public DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, File tempFile, Listener listener) { - this.sizeLimit = sizeLimit; - this.contentResolver = contentResolver; - this.tempFile = tempFile; - this.listener = listener; - } - - @Override - protected Boolean doInBackground(Uri... uris) { - boolean result = DownsizeImageTask.resize(uris, sizeLimit, contentResolver, tempFile); - if (isCancelled()) { - return false; - } - return result; - } - - @Override - protected void onPostExecute(Boolean successful) { - if (successful) { - listener.onSuccess(tempFile); - } else { - listener.onFailure(); - } - super.onPostExecute(successful); - } - - public static boolean resize(Uri[] uris, int sizeLimit, ContentResolver contentResolver, - File tempFile) { - for (Uri uri : uris) { - InputStream inputStream; - try { - inputStream = contentResolver.openInputStream(uri); - } catch (FileNotFoundException e) { - return false; - } - // Initially, just get the image dimensions. - BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = true; - BitmapFactory.decodeStream(inputStream, null, options); - IOUtils.closeQuietly(inputStream); - // Get EXIF data, for orientation info. - int orientation = getImageOrientation(uri, contentResolver); - /* Unfortunately, there isn't a determined worst case compression ratio for image - * formats. So, the only way to tell if they're too big is to compress them and - * test, and keep trying at smaller sizes. The initial estimate should be good for - * many cases, so it should only iterate once, but the loop is used to be absolutely - * sure it gets downsized to below the limit. */ - int scaledImageSize = 1024; - do { - OutputStream stream; - try { - stream = new FileOutputStream(tempFile); - } catch (FileNotFoundException e) { - return false; - } - try { - inputStream = contentResolver.openInputStream(uri); - } catch (FileNotFoundException e) { - return false; - } - options.inSampleSize = calculateInSampleSize(options, scaledImageSize, scaledImageSize); - options.inJustDecodeBounds = false; - Bitmap scaledBitmap; - try { - scaledBitmap = BitmapFactory.decodeStream(inputStream, null, options); - } catch (OutOfMemoryError error) { - return false; - } finally { - IOUtils.closeQuietly(inputStream); - } - if (scaledBitmap == null) { - return false; - } - Bitmap reorientedBitmap = reorientBitmap(scaledBitmap, orientation); - if (reorientedBitmap == null) { - scaledBitmap.recycle(); - return false; - } - Bitmap.CompressFormat format; - /* It's not likely the user will give transparent images over the upload limit, but - * if they do, make sure the transparency is retained. */ - if (!reorientedBitmap.hasAlpha()) { - format = Bitmap.CompressFormat.JPEG; - } else { - format = Bitmap.CompressFormat.PNG; - } - reorientedBitmap.compress(format, 85, stream); - reorientedBitmap.recycle(); - scaledImageSize /= 2; - } while (tempFile.length() > sizeLimit); - } - return true; - } - - /** - * Used to communicate the results of the task. - */ - public interface Listener { - void onSuccess(File file); - - void onFailure(); - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ImageDownsizer.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ImageDownsizer.kt new file mode 100644 index 00000000..a0215847 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ImageDownsizer.kt @@ -0,0 +1,101 @@ +/* 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.compose + +import android.content.ContentResolver +import android.graphics.Bitmap +import android.graphics.Bitmap.CompressFormat +import android.graphics.BitmapFactory +import android.net.Uri +import com.keylesspalace.tusky.util.IOUtils +import com.keylesspalace.tusky.util.calculateInSampleSize +import com.keylesspalace.tusky.util.getImageOrientation +import com.keylesspalace.tusky.util.reorientBitmap +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream + +/** + * @param uri the uri pointing to the input file + * @param sizeLimit the maximum number of bytes the output image is allowed to have + * @param contentResolver to resolve the specified input uri + * @param tempFile the file where the result will be stored + * @return true when the image was successfully resized, false otherwise + */ +fun downsizeImage( + uri: Uri, + sizeLimit: Int, + contentResolver: ContentResolver, + tempFile: File +): Boolean { + + val decodeBoundsInputStream = try { + contentResolver.openInputStream(uri) + } catch (e: FileNotFoundException) { + return false + } + // Initially, just get the image dimensions. + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeStream(decodeBoundsInputStream, null, options) + IOUtils.closeQuietly(decodeBoundsInputStream) + // Get EXIF data, for orientation info. + val orientation = getImageOrientation(uri, contentResolver) + /* Unfortunately, there isn't a determined worst case compression ratio for image + * formats. So, the only way to tell if they're too big is to compress them and + * test, and keep trying at smaller sizes. The initial estimate should be good for + * many cases, so it should only iterate once, but the loop is used to be absolutely + * sure it gets downsized to below the limit. */ + var scaledImageSize = 1024 + do { + val outputStream = try { + FileOutputStream(tempFile) + } catch (e: FileNotFoundException) { + return false + } + val decodeBitmapInputStream = try { + contentResolver.openInputStream(uri) + } catch (e: FileNotFoundException) { + return false + } + options.inSampleSize = calculateInSampleSize(options, scaledImageSize, scaledImageSize) + options.inJustDecodeBounds = false + val scaledBitmap: Bitmap = try { + BitmapFactory.decodeStream(decodeBitmapInputStream, null, options) + } catch (error: OutOfMemoryError) { + return false + } finally { + IOUtils.closeQuietly(decodeBitmapInputStream) + } ?: return false + + val reorientedBitmap = reorientBitmap(scaledBitmap, orientation) + if (reorientedBitmap == null) { + scaledBitmap.recycle() + return false + } + /* Retain transparency if there is any by encoding as png */ + val format: CompressFormat = if (!reorientedBitmap.hasAlpha()) { + CompressFormat.JPEG + } else { + CompressFormat.PNG + } + reorientedBitmap.compress(format, 85, outputStream) + reorientedBitmap.recycle() + scaledImageSize /= 2 + } while (tempFile.length() > sizeLimit) + + return true +} 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 0e3ac9e8..85be146c 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 @@ -32,9 +32,14 @@ import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN import com.keylesspalace.tusky.util.getImageSquarePixels import com.keylesspalace.tusky.util.getMediaSize import com.keylesspalace.tusky.util.randomAlphanumericString -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody import java.io.File @@ -72,61 +77,40 @@ class MediaUploader @Inject constructor( private val context: Context, private val mastodonApi: MastodonApi ) { - fun uploadMedia(media: QueuedMedia): Observable { - return Observable - .fromCallable { - if (shouldResizeMedia(media)) { - downsize(media) - } else media + + @OptIn(ExperimentalCoroutinesApi::class) + fun uploadMedia(media: QueuedMedia): Flow { + return flow { + if (shouldResizeMedia(media)) { + emit(downsize(media)) + } else { + emit(media) } - .switchMap { upload(it) } - .subscribeOn(Schedulers.io()) + } + .flatMapLatest { upload(it) } + .flowOn(Dispatchers.IO) } - fun prepareMedia(inUri: Uri): Single { - return Single.fromCallable { - var mediaSize = MEDIA_SIZE_UNKNOWN - var uri = inUri - var mimeType: String? = null + fun prepareMedia(inUri: Uri): PreparedMedia { + var mediaSize = MEDIA_SIZE_UNKNOWN + var uri = inUri + val mimeType: String? - try { - when (inUri.scheme) { - ContentResolver.SCHEME_CONTENT -> { + try { + when (inUri.scheme) { + ContentResolver.SCHEME_CONTENT -> { - mimeType = contentResolver.getType(uri) + mimeType = contentResolver.getType(uri) - val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp") + val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp") - contentResolver.openInputStream(inUri).use { input -> - if (input == null) { - Log.w(TAG, "Media input is null") - uri = inUri - return@use - } - val file = File.createTempFile("randomTemp1", suffix, context.cacheDir) - FileOutputStream(file.absoluteFile).use { out -> - input.copyTo(out) - uri = FileProvider.getUriForFile( - context, - BuildConfig.APPLICATION_ID + ".fileprovider", - file - ) - mediaSize = getMediaSize(contentResolver, uri) - } + contentResolver.openInputStream(inUri).use { input -> + if (input == null) { + Log.w(TAG, "Media input is null") + uri = inUri + return@use } - } - ContentResolver.SCHEME_FILE -> { - val path = uri.path - if (path == null) { - Log.w(TAG, "empty uri path $uri") - throw CouldNotOpenFileException() - } - val inputFile = File(path) - val suffix = inputFile.name.substringAfterLast('.', "tmp") - mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix) - val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir) - val input = FileInputStream(inputFile) - + val file = File.createTempFile("randomTemp1", suffix, context.cacheDir) FileOutputStream(file.absoluteFile).use { out -> input.copyTo(out) uri = FileProvider.getUriForFile( @@ -137,53 +121,74 @@ class MediaUploader @Inject constructor( mediaSize = getMediaSize(contentResolver, uri) } } - else -> { - Log.w(TAG, "Unknown uri scheme $uri") + } + ContentResolver.SCHEME_FILE -> { + val path = uri.path + if (path == null) { + Log.w(TAG, "empty uri path $uri") throw CouldNotOpenFileException() } - } - } catch (e: IOException) { - Log.w(TAG, e) - throw CouldNotOpenFileException() - } - if (mediaSize == MEDIA_SIZE_UNKNOWN) { - Log.w(TAG, "Could not determine file size of upload") - throw MediaTypeException() - } + val inputFile = File(path) + val suffix = inputFile.name.substringAfterLast('.', "tmp") + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix) + val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir) + val input = FileInputStream(inputFile) - if (mimeType != null) { - val topLevelType = mimeType.substring(0, mimeType.indexOf('/')) - when (topLevelType) { - "video" -> { - if (mediaSize > STATUS_VIDEO_SIZE_LIMIT) { - throw VideoSizeException() - } - PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize) - } - "image" -> { - PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize) - } - "audio" -> { - if (mediaSize > STATUS_AUDIO_SIZE_LIMIT) { - throw AudioSizeException() - } - PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize) - } - else -> { - throw MediaTypeException() + FileOutputStream(file.absoluteFile).use { out -> + input.copyTo(out) + uri = FileProvider.getUriForFile( + context, + BuildConfig.APPLICATION_ID + ".fileprovider", + file + ) + mediaSize = getMediaSize(contentResolver, uri) } } - } else { - Log.w(TAG, "Could not determine mime type of upload") - throw MediaTypeException() + else -> { + Log.w(TAG, "Unknown uri scheme $uri") + throw CouldNotOpenFileException() + } } + } catch (e: IOException) { + Log.w(TAG, e) + throw CouldNotOpenFileException() + } + if (mediaSize == MEDIA_SIZE_UNKNOWN) { + Log.w(TAG, "Could not determine file size of upload") + throw MediaTypeException() + } + + if (mimeType != null) { + return when (mimeType.substring(0, mimeType.indexOf('/'))) { + "video" -> { + if (mediaSize > STATUS_VIDEO_SIZE_LIMIT) { + throw VideoSizeException() + } + PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize) + } + "image" -> { + PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize) + } + "audio" -> { + if (mediaSize > STATUS_AUDIO_SIZE_LIMIT) { + throw AudioSizeException() + } + PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize) + } + else -> { + throw MediaTypeException() + } + } + } else { + Log.w(TAG, "Could not determine mime type of upload") + throw MediaTypeException() } } private val contentResolver = context.contentResolver - private fun upload(media: QueuedMedia): Observable { - return Observable.create { emitter -> + private suspend fun upload(media: QueuedMedia): Flow { + return callbackFlow { var mimeType = contentResolver.getType(media.uri) val map = MimeTypeMap.getSingleton() val fileExtension = map.getExtensionFromMimeType(mimeType) @@ -200,11 +205,11 @@ class MediaUploader @Inject constructor( var lastProgress = -1 val fileBody = ProgressRequestBody( - stream, media.mediaSize, - mimeType.toMediaTypeOrNull() + stream!!, media.mediaSize, + mimeType.toMediaTypeOrNull()!! ) { percentage -> if (percentage != lastProgress) { - emitter.onNext(UploadEvent.ProgressEvent(percentage)) + trySend(UploadEvent.ProgressEvent(percentage)) } lastProgress = percentage } @@ -217,28 +222,15 @@ class MediaUploader @Inject constructor( null } - val uploadDisposable = mastodonApi.uploadMedia(body, description) - .subscribe( - { result -> - emitter.onNext(UploadEvent.FinishedEvent(result.id)) - emitter.onComplete() - }, - { e -> - emitter.onError(e) - } - ) - - // Cancel the request when our observable is cancelled - emitter.setDisposable(uploadDisposable) + val result = mastodonApi.uploadMedia(body, description).getOrThrow() + send(UploadEvent.FinishedEvent(result.id)) + awaitClose() } } private fun downsize(media: QueuedMedia): QueuedMedia { val file = createNewImageFile(context) - DownsizeImageTask.resize( - arrayOf(media.uri), - STATUS_IMAGE_SIZE_LIMIT, context.contentResolver, file - ) + downsizeImage(media.uri, STATUS_IMAGE_SIZE_LIMIT, contentResolver, file) return media.copy(uri = file.toUri(), mediaSize = file.length()) } 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 0c15eff0..71789611 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 @@ -27,7 +27,7 @@ import android.widget.LinearLayout import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData +import androidx.lifecycle.lifecycleScope import at.connyduck.sparkbutton.helpers.Utils import com.bumptech.glide.Glide import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy @@ -35,7 +35,7 @@ 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 com.keylesspalace.tusky.util.withLifecycleContext +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 @@ -43,7 +43,7 @@ private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500 fun T.makeCaptionDialog( existingDescription: String?, previewUri: Uri, - onUpdateDescription: (String) -> LiveData + onUpdateDescription: suspend (String) -> Boolean ) where T : Activity, T : LifecycleOwner { val dialogLayout = LinearLayout(this) val padding = Utils.dpToPx(this, 8) @@ -77,12 +77,11 @@ fun T.makeCaptionDialog( input.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT)) val okListener = { dialog: DialogInterface, _: Int -> - onUpdateDescription(input.text.toString()) - withLifecycleContext { - onUpdateDescription(input.text.toString()) - .observe { success -> if (!success) showFailedCaptionMessage() } + lifecycleScope.launch { + if (!onUpdateDescription(input.text.toString())) { + showFailedCaptionMessage() + } } - dialog.dismiss() } 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 new file mode 100644 index 00000000..db6ec0e1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt @@ -0,0 +1,26 @@ +/* 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.instanceinfo + +data class InstanceInfo( + val maxChars: Int, + val pollMaxOptions: Int, + val pollMaxLength: Int, + val pollMinDuration: Int, + val pollMaxDuration: Int, + val charactersReservedPerUrl: Int, + val supportsScheduled: Boolean +) 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 new file mode 100644 index 00000000..287d5499 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt @@ -0,0 +1,104 @@ +/* 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.instanceinfo + +import android.util.Log +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.EmojisEntity +import com.keylesspalace.tusky.db.InstanceInfoEntity +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.VersionUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject + +class InstanceInfoRepository @Inject constructor( + private val api: MastodonApi, + db: AppDatabase, + accountManager: AccountManager +) { + + private val dao = db.instanceDao() + private val instanceName = accountManager.activeAccount!!.domain + + /** + * Returns the custom emojis of the instance. + * Will always try to fetch them from the api, falls back to cached Emojis in case it is not available. + * Never throws, returns empty list in case of error. + */ + suspend fun getEmojis(): List = withContext(Dispatchers.IO) { + api.getCustomEmojis() + .onSuccess { emojiList -> dao.insertOrReplace(EmojisEntity(instanceName, emojiList)) } + .getOrElse { throwable -> + Log.w(TAG, "failed to load custom emojis, falling back to cache", throwable) + dao.getEmojiInfo(instanceName)?.emojiList.orEmpty() + } + } + + /** + * Returns information about the instance. + * Will always try to fetch the most up-to-date data from the api, falls back to cache in case it is not available. + * Never throws, returns defaults of vanilla Mastodon in case of error. + */ + suspend fun getInstanceInfo(): InstanceInfo = withContext(Dispatchers.IO) { + api.getInstance() + .fold( + { instance -> + val instanceEntity = InstanceInfoEntity( + instance = instanceName, + maximumTootCharacters = instance.configuration?.statuses?.maxCharacters ?: instance.maxTootChars, + maxPollOptions = instance.configuration?.polls?.maxOptions ?: instance.pollConfiguration?.maxOptions, + maxPollOptionLength = instance.configuration?.polls?.maxCharactersPerOption ?: instance.pollConfiguration?.maxOptionChars, + minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration, + maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration, + charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl, + version = instance.version + ) + dao.insertOrReplace(instanceEntity) + instanceEntity + }, + { throwable -> + Log.w(TAG, "failed to instance, falling back to cache and default values", throwable) + dao.getInstanceInfo(instanceName) + } + ).let { instanceInfo: InstanceInfoEntity? -> + InstanceInfo( + maxChars = instanceInfo?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT, + pollMaxOptions = instanceInfo?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT, + 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, + supportsScheduled = instanceInfo?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false + ) + } + } + + companion object { + private const val TAG = "InstanceInfoRepo" + + const val DEFAULT_CHARACTER_LIMIT = 500 + private const val DEFAULT_MAX_OPTION_COUNT = 4 + private const val DEFAULT_MAX_OPTION_LENGTH = 50 + private const val DEFAULT_MIN_POLL_DURATION = 300 + private const val DEFAULT_MAX_POLL_DURATION = 604800 + + // Mastodon only counts URLs as this long in terms of status character limits + const val DEFAULT_CHARACTERS_RESERVED_PER_URL = 23 + } +} 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 52fc3aa8..9b190bc7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt @@ -19,13 +19,19 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -import io.reactivex.rxjava3.core.Single @Dao interface InstanceDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertOrReplace(instance: InstanceEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE, entity = InstanceEntity::class) + suspend fun insertOrReplace(instance: InstanceInfoEntity) + + @Insert(onConflict = OnConflictStrategy.REPLACE, entity = InstanceEntity::class) + suspend fun insertOrReplace(emojis: EmojisEntity) @Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1") - fun loadMetadataForInstance(instance: String): Single + suspend fun getInstanceInfo(instance: String): InstanceInfoEntity? + + @Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1") + suspend fun getEmojiInfo(instance: String): EmojisEntity? } 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 dd8e85d0..01767f32 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt @@ -23,7 +23,7 @@ import com.keylesspalace.tusky.entity.Emoji @Entity @TypeConverters(Converters::class) data class InstanceEntity( - @field:PrimaryKey var instance: String, + @PrimaryKey val instance: String, val emojiList: List?, val maximumTootCharacters: Int?, val maxPollOptions: Int?, @@ -33,3 +33,20 @@ data class InstanceEntity( val charactersReservedPerUrl: Int?, val version: String? ) + +@TypeConverters(Converters::class) +data class EmojisEntity( + @PrimaryKey val instance: String, + val emojiList: List? +) + +data class InstanceInfoEntity( + @PrimaryKey val instance: String, + val maximumTootCharacters: Int?, + val maxPollOptions: Int?, + val maxPollOptionLength: Int?, + val minPollDuration: Int?, + val maxPollDuration: Int?, + val charactersReservedPerUrl: Int?, + val version: String? +) 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 111cad56..2340c5dd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -77,7 +77,7 @@ interface MastodonApi { fun getLists(): Single> @GET("/api/v1/custom_emojis") - fun getCustomEmojis(): Single> + suspend fun getCustomEmojis(): Result> @GET("api/v1/instance") suspend fun getInstance(): Result @@ -145,17 +145,17 @@ interface MastodonApi { @Multipart @POST("api/v2/media") - fun uploadMedia( + suspend fun uploadMedia( @Part file: MultipartBody.Part, @Part description: MultipartBody.Part? = null - ): Single + ): Result @FormUrlEncoded @PUT("api/v1/media/{mediaId}") - fun updateMedia( + suspend fun updateMedia( @Path("mediaId") mediaId: String, @Field("description") description: String - ): Single + ): Result @POST("api/v1/statuses") fun createStatus( @@ -544,26 +544,26 @@ interface MastodonApi { ): Single @GET("api/v1/announcements") - fun listAnnouncements( + suspend fun listAnnouncements( @Query("with_dismissed") withDismissed: Boolean = true - ): Single> + ): Result> @POST("api/v1/announcements/{id}/dismiss") - fun dismissAnnouncement( + suspend fun dismissAnnouncement( @Path("id") announcementId: String - ): Single + ): Result @PUT("api/v1/announcements/{id}/reactions/{name}") - fun addAnnouncementReaction( + suspend fun addAnnouncementReaction( @Path("id") announcementId: String, @Path("name") name: String - ): Single + ): Result @DELETE("api/v1/announcements/{id}/reactions/{name}") - fun removeAnnouncementReaction( + suspend fun removeAnnouncementReaction( @Path("id") announcementId: String, @Path("name") name: String - ): Single + ): Result @FormUrlEncoded @POST("api/v1/reports") diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index 5396a21e..3a8f2f23 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -21,20 +21,19 @@ import android.widget.EditText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeViewModel -import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT -import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.EmojisEntity import com.keylesspalace.tusky.db.InstanceDao -import com.keylesspalace.tusky.db.InstanceEntity +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 import com.keylesspalace.tusky.network.MastodonApi -import io.reactivex.rxjava3.core.Single import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -94,7 +93,7 @@ class ComposeActivityTest { } apiMock = mock { - on { getCustomEmojis() } doReturn Single.just(emptyList()) + onBlocking { getCustomEmojis() } doReturn Result.success(emptyList()) onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance -> if (instance == null) { Result.failure(Throwable()) @@ -105,23 +104,25 @@ class ComposeActivityTest { } val instanceDaoMock: InstanceDao = mock { - on { loadMetadataForInstance(any()) } doReturn - Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null)) - on { loadMetadataForInstance(any()) } doReturn - Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null)) + onBlocking { getInstanceInfo(any()) } doReturn + InstanceInfoEntity(instanceDomain, null, null, null, null, null, null, null) + onBlocking { getEmojiInfo(any()) } doReturn + EmojisEntity(instanceDomain, emptyList()) } val dbMock: AppDatabase = mock { on { instanceDao() } doReturn instanceDaoMock } + val instanceInfoRepo = InstanceInfoRepository(apiMock, dbMock, accountManagerMock) + val viewModel = ComposeViewModel( apiMock, accountManagerMock, mock(), mock(), mock(), - dbMock + instanceInfoRepo ) activity.intent = Intent(activity, ComposeActivity::class.java).apply { putExtra(ComposeActivity.COMPOSE_OPTIONS_EXTRA, composeOptions) @@ -135,6 +136,7 @@ class ComposeActivityTest { activity.viewModelFactory = viewModelFactoryMock controller.create().start() + shadowOf(getMainLooper()).idle() } @Test @@ -185,7 +187,7 @@ class ComposeActivityTest { fun whenMaximumTootCharsIsNull_defaultLimitIsUsed() { instanceResponseCallback = { getInstanceWithCustomConfiguration(null) } setupActivity() - assertEquals(DEFAULT_CHARACTER_LIMIT, activity.maximumTootCharacters) + assertEquals(InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT, activity.maximumTootCharacters) } @Test @@ -236,7 +238,7 @@ class ComposeActivityTest { val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:" val additionalContent = "Check out this @image #search result: " insertSomeTextInContent(additionalContent + url) - assertEquals(activity.calculateTextLength(), additionalContent.length + DEFAULT_MAXIMUM_URL_LENGTH) + assertEquals(activity.calculateTextLength(), additionalContent.length + InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL) } @Test @@ -245,7 +247,7 @@ class ComposeActivityTest { val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:" val additionalContent = " Check out this @image #search result: " insertSomeTextInContent(shortUrl + additionalContent + url) - assertEquals(activity.calculateTextLength(), additionalContent.length + (DEFAULT_MAXIMUM_URL_LENGTH * 2)) + assertEquals(activity.calculateTextLength(), additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2)) } @Test @@ -253,7 +255,7 @@ class ComposeActivityTest { val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:" val additionalContent = " Check out this @image #search result: " insertSomeTextInContent(url + additionalContent + url) - assertEquals(activity.calculateTextLength(), additionalContent.length + (DEFAULT_MAXIMUM_URL_LENGTH * 2)) + assertEquals(activity.calculateTextLength(), additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2)) } @Test From 7499ff573329e0f039575441d6c5a68cfcaf1d77 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 21 Apr 2022 18:46:30 +0200 Subject: [PATCH 015/104] never collapse bottom app bar tabs (#2447) --- app/src/main/res/layout/activity_main.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 76e7a535..39fc717f 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -58,6 +58,7 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" app:tabIndicator="@null" + app:tabGravity="fill" app:tabMode="fixed" /> From 43709532d6b48c15ba9131e294df964363a3e893 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 21 Apr 2022 18:46:43 +0200 Subject: [PATCH 016/104] fix unparsed html in "replying to" toggle (#2448) --- .../tusky/components/drafts/DraftsActivity.kt | 3 ++- .../components/search/fragments/SearchStatusesFragment.kt | 8 ++++---- .../java/com/keylesspalace/tusky/fragment/SFragment.java | 4 +++- 3 files changed, 9 insertions(+), 6 deletions(-) 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 e580f554..db6a8a31 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 @@ -35,6 +35,7 @@ import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.databinding.ActivityDraftsBinding import com.keylesspalace.tusky.db.DraftEntity import com.keylesspalace.tusky.di.ViewModelFactory +import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.visible import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import kotlinx.coroutines.flow.collectLatest @@ -100,7 +101,7 @@ class DraftsActivity : BaseActivity(), DraftActionListener { content = draft.content, contentWarning = draft.contentWarning, inReplyToId = draft.inReplyToId, - replyingStatusContent = status.content.toString(), + replyingStatusContent = status.content.parseAsMastodonHtml().toString(), replyingStatusAuthor = status.account.localUsername, draftAttachments = draft.attachments, poll = draft.poll, 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 23ff1b07..11d90da4 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 @@ -97,7 +97,7 @@ class SearchStatusesFragment : SearchFragment(), Status } override fun onReply(position: Int) { - searchAdapter.peek(position)?.status?.let { status -> + searchAdapter.peek(position)?.let { status -> reply(status) } } @@ -199,8 +199,8 @@ class SearchStatusesFragment : SearchFragment(), Status fun newInstance() = SearchStatusesFragment() } - private fun reply(status: Status) { - val actionableStatus = status.actionableStatus + private fun reply(status: StatusViewData.Concrete) { + val actionableStatus = status.actionable val mentionedUsernames = actionableStatus.mentions.map { it.username } .toMutableSet() .apply { @@ -216,7 +216,7 @@ class SearchStatusesFragment : SearchFragment(), Status contentWarning = actionableStatus.spoilerText, mentionedUsernames = mentionedUsernames, replyingStatusAuthor = actionableStatus.account.localUsername, - replyingStatusContent = actionableStatus.content.toString() + replyingStatusContent = status.content.toString() ) ) startActivity(intent) 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 b1a47ad8..bb02807d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -15,6 +15,8 @@ package com.keylesspalace.tusky.fragment; +import static com.keylesspalace.tusky.util.StatusParsingHelper.parseAsMastodonHtml; + import android.Manifest; import android.app.DownloadManager; import android.content.ClipData; @@ -150,7 +152,7 @@ public abstract class SFragment extends Fragment implements Injectable { composeOptions.setContentWarning(contentWarning); composeOptions.setMentionedUsernames(mentionedUsernames); composeOptions.setReplyingStatusAuthor(actionableStatus.getAccount().getLocalUsername()); - composeOptions.setReplyingStatusContent(actionableStatus.getContent().toString()); + composeOptions.setReplyingStatusContent(parseAsMastodonHtml(actionableStatus.getContent()).toString()); Intent intent = ComposeActivity.startIntent(getContext(), composeOptions); getActivity().startActivity(intent); From db7eac0a8dd29d8095719669cf3d1520e17d6ef2 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 21 Apr 2022 18:46:55 +0200 Subject: [PATCH 017/104] guard against the status of a notification being null in rare cases (#2449) * guard against the status of a notification being null in rare cases * improve code, fix bug when payloads is not null * remove findViewById * add comments in NotificationsAdapter --- .../tusky/adapter/NotificationsAdapter.java | 14 ++++++-- .../tusky/adapter/StatusBaseViewHolder.java | 33 ++++++++++++++++--- .../adapter/StatusDetailedViewHolder.java | 9 ++--- .../tusky/adapter/StatusViewHolder.java | 12 +++++-- 4 files changed, 55 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 0ac029b0..f8885f36 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -180,8 +180,16 @@ public class NotificationsAdapter extends RecyclerView.Adapter { case VIEW_TYPE_STATUS: { StatusViewHolder holder = (StatusViewHolder) viewHolder; StatusViewData.Concrete status = concreteNotificaton.getStatusViewData(); - holder.setupWithStatus(status, - statusListener, statusDisplayOptions, payloadForHolder); + if (status == null) { + /* in some very rare cases servers sends null status even though they should not, + * we have to handle it somehow */ + holder.showStatusContent(false); + } else { + if (payloads == null) { + holder.showStatusContent(true); + } + holder.setupWithStatus(status, statusListener, statusDisplayOptions, payloadForHolder); + } if (concreteNotificaton.getType() == Notification.Type.POLL) { holder.setPollInfo(accountId.equals(concreteNotificaton.getAccount().getId())); } else { @@ -194,6 +202,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { StatusViewData.Concrete statusViewData = concreteNotificaton.getStatusViewData(); if (payloadForHolder == null) { if (statusViewData == null) { + /* in some very rare cases servers sends null status even though they should not, + * we have to handle it somehow */ holder.showNotificationContent(false); } else { holder.showNotificationContent(true); 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 c2729aa5..c7427450 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -20,6 +20,7 @@ import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; +import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.text.HtmlCompat; import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.LinearLayoutManager; @@ -76,6 +77,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { private SparkButton favouriteButton; private SparkButton bookmarkButton; private ImageButton moreButton; + private ConstraintLayout mediaContainer; protected MediaPreviewImageView[] mediaPreviews; private ImageView[] mediaOverlays; private TextView sensitiveMediaWarning; @@ -124,7 +126,8 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { bookmarkButton = itemView.findViewById(R.id.status_bookmark); moreButton = itemView.findViewById(R.id.status_more); - itemView.findViewById(R.id.status_media_preview_container).setClipToOutline(true); + mediaContainer = itemView.findViewById(R.id.status_media_preview_container); + mediaContainer.setClipToOutline(true); mediaPreviews = new MediaPreviewImageView[]{ itemView.findViewById(R.id.status_media_preview_0), @@ -719,9 +722,9 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { this.setupWithStatus(status, listener, statusDisplayOptions, null); } - public void setupWithStatus(StatusViewData.Concrete status, - final StatusActionListener listener, - StatusDisplayOptions statusDisplayOptions, + public void setupWithStatus(@NonNull StatusViewData.Concrete status, + @NonNull final StatusActionListener listener, + @NonNull StatusDisplayOptions statusDisplayOptions, @Nullable Object payloads) { if (payloads == null) { Status actionable = status.getActionable(); @@ -1133,6 +1136,28 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } } + public void showStatusContent(boolean show) { + int visibility = show ? View.VISIBLE : View.GONE; + avatar.setVisibility(visibility); + avatarInset.setVisibility(visibility); + displayName.setVisibility(visibility); + username.setVisibility(visibility); + timestampInfo.setVisibility(visibility); + contentWarningDescription.setVisibility(visibility); + contentWarningButton.setVisibility(visibility); + content.setVisibility(visibility); + cardView.setVisibility(visibility); + mediaContainer.setVisibility(visibility); + pollOptions.setVisibility(visibility); + pollButton.setVisibility(visibility); + pollDescription.setVisibility(visibility); + replyButton.setVisibility(visibility); + reblogButton.setVisibility(visibility); + favouriteButton.setVisibility(visibility); + 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; 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 56adfcad..1aebf1a7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -9,6 +9,7 @@ import android.view.View; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; @@ -101,10 +102,10 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { } @Override - public void setupWithStatus(final StatusViewData.Concrete status, - final StatusActionListener listener, - StatusDisplayOptions statusDisplayOptions, - @Nullable Object payloads) { + public void setupWithStatus(@NonNull final StatusViewData.Concrete status, + @NonNull final StatusActionListener listener, + @NonNull StatusDisplayOptions statusDisplayOptions, + @Nullable Object payloads) { super.setupWithStatus(status, listener, statusDisplayOptions, payloads); setupCard(status, CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status if (payloads == null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java index b054aea9..93c47564 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -22,6 +22,7 @@ import android.view.View; import android.widget.Button; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; @@ -58,9 +59,9 @@ public class StatusViewHolder extends StatusBaseViewHolder { } @Override - public void setupWithStatus(StatusViewData.Concrete status, - final StatusActionListener listener, - StatusDisplayOptions statusDisplayOptions, + public void setupWithStatus(@NonNull StatusViewData.Concrete status, + @NonNull final StatusActionListener listener, + @NonNull StatusDisplayOptions statusDisplayOptions, @Nullable Object payloads) { if (payloads == null) { @@ -129,4 +130,9 @@ public class StatusViewHolder extends StatusBaseViewHolder { content.setFilters(NO_INPUT_FILTER); } } + + public void showStatusContent(boolean show) { + super.showStatusContent(show); + contentCollapseButton.setVisibility(show ? View.VISIBLE : View.GONE); + } } From adcbe1a8317257ffe44a5540bb74dad6313fb2ae Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 21 Apr 2022 18:47:05 +0200 Subject: [PATCH 018/104] fix unparsed html in announcements (#2451) --- .../tusky/components/announcements/AnnouncementAdapter.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt index 4b5e7aa5..70ebfc7d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt @@ -32,6 +32,7 @@ import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.EmojiSpan import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.setClickableText import java.lang.ref.WeakReference @@ -60,7 +61,7 @@ class AnnouncementAdapter( val chips = holder.binding.chipGroup val addReactionChip = holder.binding.addReactionChip - val emojifiedText: CharSequence = item.content.emojify(item.emojis, text, animateEmojis) + val emojifiedText: CharSequence = item.content.parseAsMastodonHtml().emojify(item.emojis, text, animateEmojis) setClickableText(text, emojifiedText, item.mentions, item.tags, listener) From 8109a126ae988fe4347f09a588314fb618c72aa7 Mon Sep 17 00:00:00 2001 From: codl Date: Thu, 21 Apr 2022 14:40:29 +0000 Subject: [PATCH 019/104] Translated using Weblate (French) Currently translated at 100.0% (473 of 473 strings) Co-authored-by: codl Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fr/ Translation: Tusky/Tusky --- app/src/main/res/values-fr/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index e3acac34..0490c87b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -540,4 +540,8 @@ 14 jours 180 jours Rédiger un message + %s a créé un compte + Nouveaux comptes + Notifications quand quelqu\'un crée un nouveau compte + un nouveau compte a été créé \ No newline at end of file From 5243c8ddb5b78f19cecf003ae0172f3ec20309b3 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Thu, 21 Apr 2022 14:40:29 +0000 Subject: [PATCH 020/104] Translated using Weblate (Occitan) Currently translated at 93.0% (440 of 473 strings) Co-authored-by: ButterflyOfFire Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/oc/ Translation: Tusky/Tusky --- app/src/main/res/values-oc/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index c1ffa122..4dc4370f 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -337,8 +337,8 @@ %1$s Favorits - %s partatge - %s partatges + %s Partatge + %s Partatges Partejat per Aimat per From 1a921ec3945c6680a1724db49fe6307c6eb9a1a1 Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Thu, 21 Apr 2022 14:40:29 +0000 Subject: [PATCH 021/104] =?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% (477 of 477 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (473 of 473 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 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index ade13f04..351c0386 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -519,4 +519,12 @@ 365 dager 14 dager Komponer toot + %s registrerte seg + noen registrerte seg + Registreringer + Varslinger om nye brukere + %s redigerte innlegget sitt + et innlegg jeg har hatt en interaksjon med er redigert + Redigerte innlegg + Varslinger når et innlegg du har hatt en interaksjon med er redigert \ No newline at end of file From 7461315c33d360030e430baf3e20027c5d1662b1 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Thu, 21 Apr 2022 14:40:30 +0000 Subject: [PATCH 022/104] Translated using Weblate (Ukrainian) Currently translated at 100.0% (477 of 477 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (473 of 473 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 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 0031334b..d9fab3a3 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -541,4 +541,12 @@ 180 днів 365 днів Створити допис + %s реєструється + хтось реєструється + Реєстрації + Сповіщення про нових користувачів + %s редагує свій допис + допис, з яким у мене була взаємодія, відредаговано + Сповіщення, коли редагується повідомлення, з яким ви взаємодіяли + Редакції допису \ No newline at end of file From 5ecc58d9dd20ac7a7c2f355e05d095b82af70797 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Thu, 21 Apr 2022 14:40:30 +0000 Subject: [PATCH 023/104] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (477 of 477 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (473 of 473 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 | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index caf759fe..16704821 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -192,7 +192,7 @@ Ghim Trả lời Tút - Nội dung tút + Tút Xếp tab Tin nhắn Thế giới @@ -508,4 +508,12 @@ 180 ngày 365 ngày Viết tút + ai đó đăng ký trên máy chủ + %s đăng ký + Đăng ký + Thông báo về người dùng mới đăng ký + %s đã sửa tút của họ + khi một tút mà tôi tương tác bị sửa + Sửa tút + Thông báo khi tút mà tôi tương tác bị sửa \ No newline at end of file From d49785b23ba2009b9d131e3714b80003a4947d86 Mon Sep 17 00:00:00 2001 From: GunChleoc Date: Thu, 21 Apr 2022 14:40:30 +0000 Subject: [PATCH 024/104] Translated using Weblate (Gaelic) Currently translated at 99.3% (474 of 477 strings) Translated using Weblate (Gaelic) Currently translated at 100.0% (473 of 473 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 | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index bf2a2d52..16ef394d 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -253,6 +253,9 @@ Mìnich e dhan fheadhainn air a bheil cion-lèirsinn \n(%d caractar(an) air a char as fhaide) + + + Cha deach leinn am fo-thiotal a shuidheachadh A’ postadh leis a’ chunntas %1$s @@ -541,4 +544,9 @@ 14 làithean 60 latha Sgrìobh post + Chlàraich %s + Clàraidhean + Brathan mu cleachdaichean ùra + chlàraich cuideigin + Dheasaich %s am post aca \ No newline at end of file From b39588ec2a69e3ecc960c0bef234e1542a969e72 Mon Sep 17 00:00:00 2001 From: XoseM Date: Thu, 21 Apr 2022 14:40:30 +0000 Subject: [PATCH 025/104] Translated using Weblate (Galician) Currently translated at 100.0% (473 of 473 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, 4 insertions(+) diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index bfabe4c8..cf0a5083 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -519,4 +519,8 @@ 180 días 365 días Redactar publicación + %s rexistrouse + hai unha nova usuaria + Rexistros + Notificacións sobre novas usuarias \ No newline at end of file From 2a0f1c6707ce7ef20d04896f1133672e0640c1c2 Mon Sep 17 00:00:00 2001 From: Ralf Thees Date: Thu, 21 Apr 2022 14:40:30 +0000 Subject: [PATCH 026/104] Translated using Weblate (German) Currently translated at 96.6% (461 of 477 strings) Co-authored-by: Ralf Thees Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/ Translation: Tusky/Tusky --- app/src/main/res/values-de/strings.xml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 631345ae..b82f4b1d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -208,7 +208,7 @@ Neue Erwähnungen Benachrichtigungen über neue Erwähnungen Neue Folgende - Benachrichtigunen über neue Folgende + Benachrichtigungen über neue Folgende Geteilte Beiträge Benachrichtigungen, wenn deine Beiträge geteilt werden Favorisierte Beiträge @@ -528,4 +528,11 @@ 14 Tage 180 Tage Beitrag erstellen + %s hat den Beitrag bearbeitet + Ein Beitrag, mit dem ich interagiert habe, wurde bearbeitet + Registrierungen + Benachrichtigungen über neue Profile + %s hat sich registriert + Jemand hat sich registriert + Benachrichtigungen, wenn Beiträge bearbeitet werden, mit denen du interagiert hast \ No newline at end of file From 1b0f02d48c1645fb283666b4e347004484d8d68f Mon Sep 17 00:00:00 2001 From: Connyduck Date: Thu, 21 Apr 2022 14:40:30 +0000 Subject: [PATCH 027/104] Translated using Weblate (Gaelic) Currently translated at 99.3% (474 of 477 strings) Co-authored-by: Connyduck Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/gd/ Translation: Tusky/Tusky --- app/src/main/res/values-gd/strings.xml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 16ef394d..19ae1240 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -253,9 +253,12 @@ Mìnich e dhan fheadhainn air a bheil cion-lèirsinn \n(%d caractar(an) air a char as fhaide) - - - + Mìnich e dhan fheadhainn air a bheil cion-lèirsinn +\n(%d caractar(an) air a char as fhaide) + Mìnich e dhan fheadhainn air a bheil cion-lèirsinn +\n(%d caractar(an) air a char as fhaide) + Mìnich e dhan fheadhainn air a bheil cion-lèirsinn +\n(%d caractar(an) air a char as fhaide) Cha deach leinn am fo-thiotal a shuidheachadh A’ postadh leis a’ chunntas %1$s From f15b3e61bbcd89d89af0307a0993540eba333352 Mon Sep 17 00:00:00 2001 From: Constantin A <10349490+C1710@users.noreply.github.com> Date: Tue, 26 Apr 2022 18:50:58 +0200 Subject: [PATCH 028/104] New emoji picker (#2395) * Update to Emoji2 * Hopefully fix the emoji picker preference * Switch to released Filemojicompat version * Filemojicompat version as an own var * Remove an unused import * Small cleanup * Correct onDisplayPreferenceDialog; test TuskyApplication * Use TextViews instead of EmojiTextViews * Recreate the Main Activity if the emoji pack is updated * Enable coreLibraryDesugaring (for Java Streams); update Filemojicompat, downgrade Emoji2 * Update emoji font versions to 14 * Use FilemojiCompat 3.2.0-beta01 * Make ktLint happy again * Remove coreLibraryDesugaring and a FIXME * Use EmojiPickerPreference.get() * Disable emoji pack import * Update FilemojiCompat to Beta 2 * Update FilemojiCompat to Beta 3 * Update FilemojiCompat to Beta 3.2.0 final * Update FilemojiCompat to 3.2.1 --- app/build.gradle | 11 +- .../com/keylesspalace/tusky/MainActivity.kt | 42 +- .../keylesspalace/tusky/TuskyApplication.kt | 16 +- .../tusky/adapter/PollAdapter.kt | 2 +- .../components/account/AccountActivity.kt | 2 +- .../components/compose/view/EditTextTyped.kt | 2 +- .../components/preference/EmojiPreference.kt | 240 ------------ .../preference/PreferencesFragment.kt | 17 +- .../tusky/settings/SettingsDSL.kt | 10 +- .../tusky/util/EmojiCompatFont.kt | 364 ------------------ app/src/main/res/layout/activity_account.xml | 8 +- app/src/main/res/layout/activity_compose.xml | 4 +- .../main/res/layout/dialog_emojicompat.xml | 36 -- app/src/main/res/layout/item_account.xml | 2 +- .../main/res/layout/item_account_field.xml | 4 +- app/src/main/res/layout/item_announcement.xml | 2 +- .../res/layout/item_autocomplete_account.xml | 2 +- app/src/main/res/layout/item_blocked_user.xml | 2 +- app/src/main/res/layout/item_conversation.xml | 8 +- app/src/main/res/layout/item_draft.xml | 4 +- app/src/main/res/layout/item_edit_field.xml | 4 +- app/src/main/res/layout/item_follow.xml | 4 +- .../main/res/layout/item_follow_request.xml | 4 +- app/src/main/res/layout/item_muted_user.xml | 2 +- app/src/main/res/layout/item_poll.xml | 2 +- .../main/res/layout/item_report_status.xml | 12 +- .../main/res/layout/item_scheduled_status.xml | 2 +- app/src/main/res/layout/item_status.xml | 12 +- .../main/res/layout/item_status_detailed.xml | 10 +- .../res/layout/item_status_notification.xml | 8 +- .../keylesspalace/tusky/TuskyApplication.kt | 6 +- .../tusky/util/EmojiCompatFontTest.kt | 47 --- 32 files changed, 109 insertions(+), 782 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt delete mode 100644 app/src/main/res/layout/dialog_emojicompat.xml delete mode 100644 app/src/test/java/com/keylesspalace/tusky/util/EmojiCompatFontTest.kt diff --git a/app/build.gradle b/app/build.gradle index 02d7907d..4484ff54 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,6 +96,8 @@ ext.okhttpVersion = '4.9.3' ext.glideVersion = '4.13.1' ext.daggerVersion = '2.41' ext.materialdrawerVersion = '8.4.5' +ext.emoji2_version = '1.1.0' +ext.filemojicompat_version = '3.2.1' // if libraries are changed here, they should also be changed in LicenseActivity dependencies { @@ -112,8 +114,9 @@ dependencies { implementation "androidx.cardview:cardview:1.0.0" implementation "androidx.preference:preference-ktx:1.2.0" implementation "androidx.sharetarget:sharetarget:1.2.0-rc01" - implementation "androidx.emoji:emoji:1.1.0" - implementation "androidx.emoji:emoji-appcompat:1.1.0" + implementation "androidx.emoji2:emoji2:$emoji2_version" + implementation "androidx.emoji2:emoji2-views:$emoji2_version" + implementation "androidx.emoji2:emoji2-views-helper:$emoji2_version" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion" @@ -170,7 +173,9 @@ dependencies { implementation "com.github.CanHub:Android-Image-Cropper:4.1.0" - implementation "de.c1710:filemojicompat:1.0.18" + implementation "de.c1710:filemojicompat-ui:$filemojicompat_version" + implementation "de.c1710:filemojicompat:$filemojicompat_version" + implementation "de.c1710:filemojicompat-defaults:$filemojicompat_version" testImplementation "androidx.test.ext:junit:1.1.3" testImplementation "org.robolectric:robolectric:4.4" diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 3f559399..3e203059 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -35,8 +35,8 @@ import androidx.appcompat.app.AlertDialog import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat import androidx.core.content.pm.ShortcutManagerCompat -import androidx.emoji.text.EmojiCompat -import androidx.emoji.text.EmojiCompat.InitCallback +import androidx.core.view.GravityCompat +import androidx.emoji2.text.EmojiCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager @@ -114,6 +114,7 @@ import com.mikepenz.materialdrawer.util.updateBadge import com.mikepenz.materialdrawer.widget.AccountHeaderView import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector +import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.launch @@ -150,13 +151,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje private var accountLocked: Boolean = false - private val emojiInitCallback = object : InitCallback() { - override fun onInitialized() { - if (!isDestroyed) { - updateProfiles() - } - } - } + // We need to know if the emoji pack has been changed + private var selectedEmojiPack: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -271,11 +267,31 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje // Flush old media that was cached for sharing deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky")) } + + selectedEmojiPack = preferences.getString(EMOJI_PREFERENCE, "") } override fun onResume() { super.onResume() NotificationHelper.clearNotificationsForActiveAccount(this, accountManager) + val currentEmojiPack = preferences.getString(EMOJI_PREFERENCE, "") + if (currentEmojiPack != selectedEmojiPack) { + Log.d( + TAG, + "onResume: EmojiPack has been changed from %s to %s" + .format(selectedEmojiPack, currentEmojiPack) + ) + selectedEmojiPack = currentEmojiPack + recreate() + } + } + + override fun onStart() { + super.onStart() + // For some reason the navigation drawer is opened when the activity is recreated + if (binding.mainDrawerLayout.isOpen) { + binding.mainDrawerLayout.closeDrawer(GravityCompat.START, false) + } } override fun onBackPressed() { @@ -333,11 +349,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } } - override fun onDestroy() { - super.onDestroy() - EmojiCompat.get().unregisterInitCallback(emojiInitCallback) - } - private fun forwardShare(intent: Intent) { val composeIntent = Intent(this, ComposeActivity::class.java) composeIntent.action = intent.action @@ -530,7 +541,6 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje } ) } - EmojiCompat.get().registerInitCallback(emojiInitCallback) } override fun onSaveInstanceState(outState: Bundle) { @@ -800,7 +810,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje private fun updateProfiles() { val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) val profiles: MutableList = accountManager.getAllAccountsOrderedByActive().map { acc -> - val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis)) + val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis))!! ProfileDrawerItem().apply { isSelected = acc.isActive diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt index 0339a7bc..ded947a8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -19,18 +19,18 @@ import android.app.Application import android.content.Context import android.content.res.Configuration import android.util.Log -import androidx.emoji.text.EmojiCompat import androidx.preference.PreferenceManager import androidx.work.WorkManager import autodispose2.AutoDisposePlugins import com.keylesspalace.tusky.components.notifications.NotificationWorkerFactory import com.keylesspalace.tusky.di.AppInjector -import com.keylesspalace.tusky.settings.PrefKeys -import com.keylesspalace.tusky.util.EmojiCompatFont import com.keylesspalace.tusky.util.LocaleManager import com.keylesspalace.tusky.util.ThemeUtils import dagger.android.DispatchingAndroidInjector import dagger.android.HasAndroidInjector +import de.c1710.filemojicompat_defaults.DefaultEmojiPackList +import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper +import de.c1710.filemojicompat_ui.helpers.EmojiPreference import io.reactivex.rxjava3.plugins.RxJavaPlugins import org.conscrypt.Conscrypt import java.security.Security @@ -65,12 +65,10 @@ class TuskyApplication : Application(), HasAndroidInjector { val preferences = PreferenceManager.getDefaultSharedPreferences(this) - // init the custom emoji fonts - val emojiSelection = preferences.getInt(PrefKeys.EMOJI, 0) - val emojiConfig = EmojiCompatFont.byId(emojiSelection) - .getConfig(this) - .setReplaceAll(true) - EmojiCompat.init(emojiConfig) + // In this case, we want to have the emoji preferences merged with the other ones + // Copied from PreferenceManager.getDefaultSharedPreferenceName + EmojiPreference.sharedPreferenceName = packageName + "_preferences" + EmojiPackHelper.init(this, DefaultEmojiPackList.get(this), allowPackImports = false) // init night mode val theme = preferences.getString("appTheme", ThemeUtils.APP_THEME_DEFAULT) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt index 1a60d860..9ffeca9e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt @@ -19,7 +19,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat -import androidx.emoji.text.EmojiCompat +import androidx.emoji2.text.EmojiCompat import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.ItemPollBinding 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 ddecd20c..7a7d5ecc 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 @@ -37,7 +37,7 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat.Type.systemBars import androidx.core.view.updatePadding -import androidx.emoji.text.EmojiCompat +import androidx.emoji2.text.EmojiCompat import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewpager2.widget.MarginPageTransformer diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt index dca696d8..2a1c7446 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt @@ -26,7 +26,7 @@ import androidx.core.view.OnReceiveContentListener import androidx.core.view.ViewCompat import androidx.core.view.inputmethod.EditorInfoCompat import androidx.core.view.inputmethod.InputConnectionCompat -import androidx.emoji.widget.EmojiEditTextHelper +import androidx.emoji2.viewsintegration.EmojiEditTextHelper class EditTextTyped @JvmOverloads constructor( context: Context, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt deleted file mode 100644 index 47cb37ae..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/EmojiPreference.kt +++ /dev/null @@ -1,240 +0,0 @@ -package com.keylesspalace.tusky.components.preference - -import android.app.AlarmManager -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.os.Build -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.widget.RadioButton -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import androidx.preference.Preference -import androidx.preference.PreferenceManager -import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.SplashActivity -import com.keylesspalace.tusky.components.notifications.NotificationHelper -import com.keylesspalace.tusky.databinding.DialogEmojicompatBinding -import com.keylesspalace.tusky.databinding.ItemEmojiPrefBinding -import com.keylesspalace.tusky.util.EmojiCompatFont -import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.BLOBMOJI -import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.FONTS -import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.NOTOEMOJI -import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.SYSTEM_DEFAULT -import com.keylesspalace.tusky.util.EmojiCompatFont.Companion.TWEMOJI -import com.keylesspalace.tusky.util.hide -import com.keylesspalace.tusky.util.show -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.disposables.Disposable -import okhttp3.OkHttpClient -import kotlin.system.exitProcess - -/** - * This Preference lets the user select their preferred emoji font - */ -class EmojiPreference( - context: Context, - private val okHttpClient: OkHttpClient -) : Preference(context) { - - private lateinit var selected: EmojiCompatFont - private lateinit var original: EmojiCompatFont - private val radioButtons = mutableListOf() - private var updated = false - private var currentNeedsUpdate = false - - private val downloadDisposables = MutableList(FONTS.size) { null } - - override fun onAttachedToHierarchy(preferenceManager: PreferenceManager) { - super.onAttachedToHierarchy(preferenceManager) - - // Find out which font is currently active - selected = EmojiCompatFont.byId( - PreferenceManager.getDefaultSharedPreferences(context).getInt(key, 0) - ) - // We'll use this later to determine if anything has changed - original = selected - summary = selected.getDisplay(context) - } - - override fun onClick() { - val binding = DialogEmojicompatBinding.inflate(LayoutInflater.from(context)) - - setupItem(BLOBMOJI, binding.itemBlobmoji) - setupItem(TWEMOJI, binding.itemTwemoji) - setupItem(NOTOEMOJI, binding.itemNotoemoji) - setupItem(SYSTEM_DEFAULT, binding.itemNomoji) - - AlertDialog.Builder(context) - .setView(binding.root) - .setPositiveButton(android.R.string.ok) { _, _ -> onDialogOk() } - .setNegativeButton(android.R.string.cancel, null) - .show() - } - - private fun setupItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { - // Initialize all the views - binding.emojiName.text = font.getDisplay(context) - binding.emojiCaption.setText(font.caption) - binding.emojiThumbnail.setImageResource(font.img) - - // There needs to be a list of all the radio buttons in order to uncheck them when one is selected - radioButtons.add(binding.emojiRadioButton) - updateItem(font, binding) - - // Set actions - binding.emojiDownload.setOnClickListener { startDownload(font, binding) } - binding.emojiDownloadCancel.setOnClickListener { cancelDownload(font, binding) } - binding.emojiRadioButton.setOnClickListener { radioButton: View -> select(font, radioButton as RadioButton) } - binding.root.setOnClickListener { - select(font, binding.emojiRadioButton) - } - } - - private fun startDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { - // Switch to downloading style - binding.emojiDownload.hide() - binding.emojiCaption.visibility = View.INVISIBLE - binding.emojiProgress.show() - binding.emojiProgress.progress = 0 - binding.emojiDownloadCancel.show() - font.downloadFontFile(context, okHttpClient) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { progress -> - // The progress is returned as a float between 0 and 1, or -1 if it could not determined - if (progress >= 0) { - binding.emojiProgress.isIndeterminate = false - val max = binding.emojiProgress.max.toFloat() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - binding.emojiProgress.setProgress((max * progress).toInt(), true) - } else { - binding.emojiProgress.progress = (max * progress).toInt() - } - } else { - binding.emojiProgress.isIndeterminate = true - } - }, - { - Toast.makeText(context, R.string.download_failed, Toast.LENGTH_SHORT).show() - updateItem(font, binding) - }, - { - finishDownload(font, binding) - } - ).also { downloadDisposables[font.id] = it } - } - - private fun cancelDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { - font.deleteDownloadedFile(context) - downloadDisposables[font.id]?.dispose() - downloadDisposables[font.id] = null - updateItem(font, binding) - } - - private fun finishDownload(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { - select(font, binding.emojiRadioButton) - updateItem(font, binding) - // Set the flag to restart the app (because an update has been downloaded) - if (selected === original && currentNeedsUpdate) { - updated = true - currentNeedsUpdate = false - } - } - - /** - * Select a font both visually and logically - * - * @param font The font to be selected - * @param radio The radio button associated with it's visual item - */ - private fun select(font: EmojiCompatFont, radio: RadioButton) { - selected = font - radioButtons.forEach { radioButton -> - radioButton.isChecked = radioButton == radio - } - } - - /** - * Called when a "consistent" state is reached, i.e. it's not downloading the font - * - * @param font The font to be displayed - * @param binding The ItemEmojiPrefBinding to show the item in - */ - private fun updateItem(font: EmojiCompatFont, binding: ItemEmojiPrefBinding) { - // There's no download going on - binding.emojiProgress.hide() - binding.emojiDownloadCancel.hide() - binding.emojiCaption.show() - if (font.isDownloaded(context)) { - // Make it selectable - binding.emojiDownload.hide() - binding.emojiRadioButton.show() - binding.root.isClickable = true - } else { - // Make it downloadable - binding.emojiDownload.show() - binding.emojiRadioButton.hide() - binding.root.isClickable = false - } - - // Select it if necessary - if (font === selected) { - binding.emojiRadioButton.isChecked = true - // Update available - if (!font.isDownloaded(context)) { - currentNeedsUpdate = true - } - } else { - binding.emojiRadioButton.isChecked = false - } - } - - private fun saveSelectedFont() { - val index = selected.id - Log.i(TAG, "saveSelectedFont: Font ID: $index") - PreferenceManager - .getDefaultSharedPreferences(context) - .edit() - .putInt(key, index) - .apply() - summary = selected.getDisplay(context) - } - - /** - * User clicked ok -> save the selected font and offer to restart the app if something changed - */ - private fun onDialogOk() { - saveSelectedFont() - if (selected !== original || updated) { - AlertDialog.Builder(context) - .setTitle(R.string.restart_required) - .setMessage(R.string.restart_emoji) - .setNegativeButton(R.string.later, null) - .setPositiveButton(R.string.restart) { _, _ -> - // Restart the app - // From https://stackoverflow.com/a/17166729/5070653 - val launchIntent = Intent(context, SplashActivity::class.java) - val mPendingIntent = PendingIntent.getActivity( - context, - 0x1f973, // This is the codepoint of the party face emoji :D - launchIntent, - NotificationHelper.pendingIntentFlags(false) - ) - val mgr = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager - mgr.set( - AlarmManager.RTC, - System.currentTimeMillis() + 100, - mPendingIntent - ) - exitProcess(0) - }.show() - } - } - - companion object { - private const val TAG = "EmojiPreference" - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt index 05438b72..74d8c815 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt @@ -38,14 +38,11 @@ import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizePx -import okhttp3.OkHttpClient +import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference import javax.inject.Inject class PreferencesFragment : PreferenceFragmentCompat(), Injectable { - @Inject - lateinit var okhttpclient: OkHttpClient - @Inject lateinit var accountManager: AccountManager @@ -65,11 +62,7 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable { icon = makeIcon(GoogleMaterial.Icon.gmd_palette) } - emojiPreference(okhttpclient) { - setDefaultValue("system_default") - setIcon(R.drawable.ic_emoji_24dp) - key = PrefKeys.EMOJI - setSummary(R.string.system_default) + emojiPreference(requireActivity()) { setTitle(R.string.emoji_style) icon = makeIcon(GoogleMaterial.Icon.gmd_sentiment_satisfied) } @@ -300,6 +293,12 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable { } } + override fun onDisplayPreferenceDialog(preference: Preference) { + if (!EmojiPickerPreference.onDisplayPreferenceDialog(this, preference)) { + super.onDisplayPreferenceDialog(preference) + } + } + companion object { fun newInstance(): PreferencesFragment { return PreferencesFragment() diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt index 1569cb15..85270081 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt @@ -1,7 +1,9 @@ package com.keylesspalace.tusky.settings import android.content.Context +import androidx.activity.result.ActivityResultRegistryOwner import androidx.annotation.StringRes +import androidx.lifecycle.LifecycleOwner import androidx.preference.CheckBoxPreference import androidx.preference.EditTextPreference import androidx.preference.ListPreference @@ -10,8 +12,7 @@ import androidx.preference.PreferenceCategory import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreference -import com.keylesspalace.tusky.components.preference.EmojiPreference -import okhttp3.OkHttpClient +import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference class PreferenceParent( val context: Context, @@ -32,8 +33,9 @@ inline fun PreferenceParent.listPreference(builder: ListPreference.() -> Unit): return pref } -inline fun PreferenceParent.emojiPreference(okHttpClient: OkHttpClient, builder: EmojiPreference.() -> Unit): EmojiPreference { - val pref = EmojiPreference(context, okHttpClient) +inline fun PreferenceParent.emojiPreference(activity: A, builder: EmojiPickerPreference.() -> Unit): EmojiPickerPreference + where A : Context, A : ActivityResultRegistryOwner, A : LifecycleOwner { + val pref = EmojiPickerPreference.get(activity) builder(pref) addPref(pref) return pref diff --git a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt b/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt deleted file mode 100644 index 385be6c1..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/EmojiCompatFont.kt +++ /dev/null @@ -1,364 +0,0 @@ -package com.keylesspalace.tusky.util - -import android.content.Context -import android.util.Log -import android.util.Pair -import androidx.annotation.DrawableRes -import androidx.annotation.StringRes -import androidx.annotation.VisibleForTesting -import com.keylesspalace.tusky.R -import de.c1710.filemojicompat.FileEmojiCompatConfig -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.ObservableEmitter -import io.reactivex.rxjava3.schedulers.Schedulers -import okhttp3.OkHttpClient -import okhttp3.Request -import okhttp3.Response -import okhttp3.ResponseBody -import okhttp3.internal.toLongOrDefault -import okio.Source -import okio.buffer -import okio.sink -import java.io.EOFException -import java.io.File -import java.io.FilenameFilter -import java.io.IOException -import kotlin.math.max - -/** - * This class bundles information about an emoji font as well as many convenient actions. - */ -class EmojiCompatFont( - val name: String, - private val display: String, - @StringRes val caption: Int, - @DrawableRes val img: Int, - val url: String, - // The version is stored as a String in the x.xx.xx format (to be able to compare versions) - val version: String -) { - - private val versionCode = getVersionCode(version) - - // A list of all available font files and whether they are older than the current version or not - // They are ordered by their version codes in ascending order - private var existingFontFileCache: List>>? = null - - val id: Int - get() = FONTS.indexOf(this) - - fun getDisplay(context: Context): String { - return if (this !== SYSTEM_DEFAULT) display else context.getString(R.string.system_default) - } - - /** - * This method will return the actual font file (regardless of its existence) for - * the current version (not necessarily the latest!). - * - * @return The font (TTF) file or null if called on SYSTEM_FONT - */ - private fun getFontFile(context: Context): File? { - return if (this !== SYSTEM_DEFAULT) { - val directory = File(context.getExternalFilesDir(null), DIRECTORY) - File(directory, "$name$version.ttf") - } else { - null - } - } - - fun getConfig(context: Context): FileEmojiCompatConfig { - return FileEmojiCompatConfig(context, getLatestFontFile(context)) - } - - fun isDownloaded(context: Context): Boolean { - return this === SYSTEM_DEFAULT || getFontFile(context)?.exists() == true || fontFileExists(context) - } - - /** - * Checks whether there is already a font version that satisfies the current version, i.e. it - * has a higher or equal version code. - * - * @param context The Context - * @return Whether there is a font file with a higher or equal version code to the current - */ - private fun fontFileExists(context: Context): Boolean { - val existingFontFiles = getExistingFontFiles(context) - return if (existingFontFiles.isNotEmpty()) { - compareVersions(existingFontFiles.last().second, versionCode) >= 0 - } else { - false - } - } - - /** - * Deletes any older version of a font - * - * @param context The current Context - */ - private fun deleteOldVersions(context: Context) { - val existingFontFiles = getExistingFontFiles(context) - Log.d(TAG, "deleting old versions...") - Log.d(TAG, String.format("deleteOldVersions: Found %d other font files", existingFontFiles.size)) - for (fileExists in existingFontFiles) { - if (compareVersions(fileExists.second, versionCode) < 0) { - val file = fileExists.first - // Uses side effects! - Log.d( - TAG, - String.format( - "Deleted %s successfully: %s", file.absolutePath, - file.delete() - ) - ) - } - } - } - - /** - * Loads all font files that are inside the files directory into an ArrayList with the information - * on whether they are older than the currently available version or not. - * - * @param context The Context - */ - private fun getExistingFontFiles(context: Context): List>> { - // Only load it once - existingFontFileCache?.let { - return it - } - // If we call this on the system default font, just return nothing... - if (this === SYSTEM_DEFAULT) { - existingFontFileCache = emptyList() - return emptyList() - } - - val directory = File(context.getExternalFilesDir(null), DIRECTORY) - // It will search for old versions using a regex that matches the font's name plus - // (if present) a version code. No version code will be regarded as version 0. - val fontRegex = "$name(\\d+(\\.\\d+)*)?\\.ttf".toPattern() - val ttfFilter = FilenameFilter { _, name: String -> name.endsWith(".ttf") } - val foundFontFiles = directory.listFiles(ttfFilter).orEmpty() - Log.d( - TAG, - String.format( - "loadExistingFontFiles: %d other font files found", - foundFontFiles.size - ) - ) - - return foundFontFiles.map { file -> - val matcher = fontRegex.matcher(file.name) - val versionCode = if (matcher.matches()) { - val version = matcher.group(1) - getVersionCode(version) - } else { - listOf(0) - } - Pair(file, versionCode) - }.sortedWith { a, b -> - compareVersions(a.second, b.second) - }.also { - existingFontFileCache = it - } - } - - /** - * Returns the current or latest version of this font file (if there is any) - * - * @param context The Context - * @return The file for this font with the current or (if not existent) highest version code or null if there is no file for this font. - */ - private fun getLatestFontFile(context: Context): File? { - val current = getFontFile(context) - if (current != null && current.exists()) return current - val existingFontFiles = getExistingFontFiles(context) - return existingFontFiles.firstOrNull()?.first - } - - private fun getVersionCode(version: String?): List { - if (version == null) return listOf(0) - return version.split(".").map { - it.toIntOrNull() ?: 0 - } - } - - fun downloadFontFile( - context: Context, - okHttpClient: OkHttpClient - ): Observable { - return Observable.create { emitter: ObservableEmitter -> - // It is possible (and very likely) that the file does not exist yet - val downloadFile = getFontFile(context)!! - if (!downloadFile.exists()) { - downloadFile.parentFile?.mkdirs() - downloadFile.createNewFile() - } - val request = Request.Builder().url(url) - .build() - - val sink = downloadFile.sink().buffer() - var source: Source? = null - try { - // Download! - val response = okHttpClient.newCall(request).execute() - - val responseBody = response.body - if (response.isSuccessful && responseBody != null) { - val size = response.length() - var progress = 0f - source = responseBody.source() - try { - while (!emitter.isDisposed) { - sink.write(source, CHUNK_SIZE) - progress += CHUNK_SIZE.toFloat() - if (size > 0) { - emitter.onNext(progress / size) - } else { - emitter.onNext(-1f) - } - } - } catch (ex: EOFException) { - /* - This means we've finished downloading the file since sink.write - will throw an EOFException when the file to be read is empty. - */ - } - } else { - Log.e(TAG, "Downloading $url failed. Status code: ${response.code}") - emitter.tryOnError(Exception()) - } - } catch (ex: IOException) { - Log.e(TAG, "Downloading $url failed.", ex) - downloadFile.deleteIfExists() - emitter.tryOnError(ex) - } finally { - source?.close() - sink.close() - if (emitter.isDisposed) { - downloadFile.deleteIfExists() - } else { - deleteOldVersions(context) - emitter.onComplete() - } - } - } - .subscribeOn(Schedulers.io()) - } - - /** - * Deletes the downloaded file, if it exists. Should be called when a download gets cancelled. - */ - fun deleteDownloadedFile(context: Context) { - getFontFile(context)?.deleteIfExists() - } - - override fun toString(): String { - return display - } - - companion object { - private const val TAG = "EmojiCompatFont" - - /** - * This String represents the sub-directory the fonts are stored in. - */ - private const val DIRECTORY = "emoji" - - private const val CHUNK_SIZE = 4096L - - // The system font gets some special behavior... - val SYSTEM_DEFAULT = EmojiCompatFont( - "system-default", - "System Default", - R.string.caption_systememoji, - R.drawable.ic_emoji_34dp, - "", - "0" - ) - val BLOBMOJI = EmojiCompatFont( - "Blobmoji", - "Blobmoji", - R.string.caption_blobmoji, - R.drawable.ic_blobmoji, - "https://tusky.app/hosted/emoji/BlobmojiCompat.ttf", - "14.0.1" - ) - val TWEMOJI = EmojiCompatFont( - "Twemoji", - "Twemoji", - R.string.caption_twemoji, - R.drawable.ic_twemoji, - "https://tusky.app/hosted/emoji/TwemojiCompat.ttf", - "14.0.0" - ) - val NOTOEMOJI = EmojiCompatFont( - "NotoEmoji", - "Noto Emoji", - R.string.caption_notoemoji, - R.drawable.ic_notoemoji, - "https://tusky.app/hosted/emoji/NotoEmojiCompat.ttf", - "14.0.0" - ) - - /** - * This array stores all available EmojiCompat fonts. - * References to them can simply be saved by saving their indices - */ - val FONTS = listOf(SYSTEM_DEFAULT, BLOBMOJI, TWEMOJI, NOTOEMOJI) - - /** - * Returns the Emoji font associated with this ID - * - * @param id the ID of this font - * @return the corresponding font. Will default to SYSTEM_DEFAULT if not in range. - */ - fun byId(id: Int): EmojiCompatFont = FONTS.getOrElse(id) { SYSTEM_DEFAULT } - - /** - * Compares two version codes to each other - * - * @param versionA The first version - * @param versionB The second version - * @return -1 if versionA < versionB, 1 if versionA > versionB and 0 otherwise - */ - @VisibleForTesting - fun compareVersions(versionA: List, versionB: List): Int { - val len = max(versionB.size, versionA.size) - for (i in 0 until len) { - - val vA = versionA.getOrElse(i) { 0 } - val vB = versionB.getOrElse(i) { 0 } - - // It needs to be decided on the next level - if (vA == vB) continue - // Okay, is version B newer or version A? - return vA.compareTo(vB) - } - - // The versions are equal - return 0 - } - - /** - * This method is needed because when transparent compression is used OkHttp reports - * [ResponseBody.contentLength] as -1. We try to get the header which server sent - * us manually here. - * - * @see [OkHttp issue 259](https://github.com/square/okhttp/issues/259) - */ - private fun Response.length(): Long { - networkResponse?.let { - val header = it.header("Content-Length") ?: return -1 - return header.toLongOrDefault(-1) - } - - // In case it's a fully cached response - return body?.contentLength() ?: -1 - } - - private fun File.deleteIfExists() { - if (exists() && !delete()) { - Log.e(TAG, "Could not delete file $this") - } - } - } -} diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml index 89a56d9e..31b37ad8 100644 --- a/app/src/main/res/layout/activity_account.xml +++ b/app/src/main/res/layout/activity_account.xml @@ -112,7 +112,7 @@ app:layout_constraintStart_toStartOf="@id/guideAvatar" app:layout_constraintTop_toTopOf="@+id/accountFollowButton" /> - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_account.xml b/app/src/main/res/layout/item_account.xml index a7b3a0ef..c1565e09 100644 --- a/app/src/main/res/layout/item_account.xml +++ b/app/src/main/res/layout/item_account.xml @@ -32,7 +32,7 @@ tools:src="#000" tools:visibility="visible" /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Date: Wed, 27 Apr 2022 11:58:39 -0600 Subject: [PATCH 029/104] Remove Identity Proof Support (#2456) * Remove IdentityProof.kt and refactor * Remove accountFieldData from viewmodel * Remove unused imports --- .../components/account/AccountActivity.kt | 8 +---- .../components/account/AccountFieldAdapter.kt | 32 +++++-------------- .../components/account/AccountViewModel.kt | 28 ---------------- .../tusky/entity/IdentityProof.kt | 9 ------ .../tusky/network/MastodonApi.kt | 6 ---- 5 files changed, 9 insertions(+), 74 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/entity/IdentityProof.kt 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 7a7d5ecc..cc8719bb 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 @@ -372,12 +372,6 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI .show() } } - viewModel.accountFieldData.observe( - this - ) { - accountFieldAdapter.fields = it - accountFieldAdapter.notifyDataSetChanged() - } viewModel.noteSaved.observe(this) { binding.saveNoteInfo.visible(it, View.INVISIBLE) } @@ -409,7 +403,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI val emojifiedNote = account.note.parseAsMastodonHtml().emojify(account.emojis, binding.accountNoteTextView, animateEmojis) setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this) - // accountFieldAdapter.fields = account.fields ?: emptyList() + accountFieldAdapter.fields = account.fields ?: emptyList() accountFieldAdapter.emojis = account.emojis ?: emptyList() accountFieldAdapter.notifyDataSetChanged() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt index d51bb145..86acb813 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt @@ -15,7 +15,6 @@ package com.keylesspalace.tusky.components.account -import android.text.method.LinkMovementMethod import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView @@ -23,11 +22,8 @@ import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.Field -import com.keylesspalace.tusky.entity.IdentityProof import com.keylesspalace.tusky.interfaces.LinkListener import com.keylesspalace.tusky.util.BindingHolder -import com.keylesspalace.tusky.util.Either -import com.keylesspalace.tusky.util.createClickableText import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.setClickableText @@ -38,7 +34,7 @@ class AccountFieldAdapter( ) : RecyclerView.Adapter>() { var emojis: List = emptyList() - var fields: List> = emptyList() + var fields: List = emptyList() override fun getItemCount() = fields.size @@ -48,32 +44,20 @@ class AccountFieldAdapter( } override fun onBindViewHolder(holder: BindingHolder, position: Int) { - val proofOrField = fields[position] + val field = fields[position] val nameTextView = holder.binding.accountFieldName val valueTextView = holder.binding.accountFieldValue - if (proofOrField.isLeft()) { - val identityProof = proofOrField.asLeft() + val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis) + nameTextView.text = emojifiedName - nameTextView.text = identityProof.provider - valueTextView.text = createClickableText(identityProof.username, identityProof.profileUrl) - - valueTextView.movementMethod = LinkMovementMethod.getInstance() + val emojifiedValue = field.value.parseAsMastodonHtml().emojify(emojis, valueTextView, animateEmojis) + setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener) + if (field.verifiedAt != null) { valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) } else { - val field = proofOrField.asRight() - val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis) - nameTextView.text = emojifiedName - - val emojifiedValue = field.value.parseAsMastodonHtml().emojify(emojis, valueTextView, animateEmojis) - setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener) - - if (field.verifiedAt != null) { - valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_check_circle, 0) - } else { - valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) - } + valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt index 6fa988ac..664651eb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt @@ -10,17 +10,13 @@ import com.keylesspalace.tusky.appstore.ProfileEditedEvent import com.keylesspalace.tusky.appstore.UnfollowEvent import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.entity.Account -import com.keylesspalace.tusky.entity.Field -import com.keylesspalace.tusky.entity.IdentityProof import com.keylesspalace.tusky.entity.Relationship import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.Error import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.RxAwareViewModel import com.keylesspalace.tusky.util.Success -import com.keylesspalace.tusky.util.combineOptionalLiveData import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.Disposable import retrofit2.Call @@ -40,13 +36,6 @@ class AccountViewModel @Inject constructor( val noteSaved = MutableLiveData() - private val identityProofData = MutableLiveData>() - - val accountFieldData = combineOptionalLiveData(accountData, identityProofData) { accountRes, identityProofs -> - identityProofs.orEmpty().map { Either.Left(it) } - .plus(accountRes?.data?.fields.orEmpty().map { Either.Right(it) }) - } - val isRefreshing = MutableLiveData().apply { value = false } private var isDataLoading = false @@ -106,22 +95,6 @@ class AccountViewModel @Inject constructor( } } - private fun obtainIdentityProof(reload: Boolean = false) { - if (identityProofData.value == null || reload) { - - mastodonApi.identityProofs(accountId) - .subscribe( - { proofs -> - identityProofData.postValue(proofs) - }, - { t -> - Log.w(TAG, "failed obtaining identity proofs", t) - } - ) - .autoDispose() - } - } - fun changeFollowState() { val relationship = relationshipData.value?.data if (relationship?.following == true || relationship?.requested == true) { @@ -314,7 +287,6 @@ class AccountViewModel @Inject constructor( return accountId.let { obtainAccount(isReload) - obtainIdentityProof() if (!isSelf) obtainRelationship(isReload) } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/IdentityProof.kt b/app/src/main/java/com/keylesspalace/tusky/entity/IdentityProof.kt deleted file mode 100644 index 98af734b..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/entity/IdentityProof.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.keylesspalace.tusky.entity - -import com.google.gson.annotations.SerializedName - -data class IdentityProof( - val provider: String, - @SerializedName("provider_username") val username: String, - @SerializedName("profile_url") val profileUrl: String -) 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 2340c5dd..02af5caa 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -24,7 +24,6 @@ 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.IdentityProof import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.Marker import com.keylesspalace.tusky.entity.MastoList @@ -367,11 +366,6 @@ interface MastodonApi { @Query("id[]") accountIds: List ): Single> - @GET("api/v1/accounts/{id}/identity_proofs") - fun identityProofs( - @Path("id") accountId: String - ): Single> - @POST("api/v1/pleroma/accounts/{id}/subscribe") fun subscribeAccount( @Path("id") accountId: String From f34aaa889d8663acb351e65d172bdfe5f806763e Mon Sep 17 00:00:00 2001 From: codl Date: Thu, 28 Apr 2022 16:13:28 +0000 Subject: [PATCH 030/104] Translated using Weblate (French) Currently translated at 100.0% (477 of 477 strings) Co-authored-by: codl Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fr/ Translation: Tusky/Tusky --- app/src/main/res/values-fr/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0490c87b..248d4f0d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -544,4 +544,8 @@ Nouveaux comptes Notifications quand quelqu\'un crée un nouveau compte un nouveau compte a été créé + %s a modifié son message + un message avec lequel j\'ai interagi est modifié + Messages modifiés + Notifications quand un post avec lequel vous avez interagi est modifié \ No newline at end of file From 3a7f31f833eabe3ac9a460b3482ed642a62a160d Mon Sep 17 00:00:00 2001 From: "Gera, Zoltan" Date: Thu, 28 Apr 2022 16:13:28 +0000 Subject: [PATCH 031/104] Translated using Weblate (Hungarian) Currently translated at 100.0% (477 of 477 strings) Translated using Weblate (Hungarian) Currently translated at 100.0% (477 of 477 strings) Co-authored-by: Gera, Zoltan Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/hu/ Translation: Tusky/Tusky --- app/src/main/res/values-hu/strings.xml | 128 ++++++++++++++----------- 1 file changed, 71 insertions(+), 57 deletions(-) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 17e4e788..18a4bb8a 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -9,26 +9,26 @@ Azonosítatlan engedélyezési hiba történt. Engedély megtagadva. Bejelentkezési token megszerzése sikertelen. - Túl hosszú a tülkölés! + 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. Média olvasási engedély szükséges. Média tárolási engedély szükséges. - Képek és videók egyszerre nem csatolhatók ugyanazon tülköléshez. + Képek és videók egyszerre nem csatolhatóak ugyanazon bejegyzéshez. Feltöltés sikertelen. - Nem sikerült elküldeni a tülköt. + Nem sikerült elküldeni a bejegyzést. Kezdőlap Értesítések Helyi Föderációs Közvetlen üzenetek Fülek - Tülk - Tülkök + Szál + Bejegyzések Válaszokkal - Rögzített + Kitűzött Követett Követő Kedvencek @@ -40,17 +40,17 @@ Licenszek \@%s %s megtolta - Kényes tartalom + Érzékeny tartalom Rejtett média - Kattints a megnézéshez + Kattints a megtekintéshez Mutass többet Mutass kevesebbet Kibontás Összecsukás Nincs itt semmi. Üres tartalom. Húzd le a frissítéshez! - %s megtolta a tülködet - %s kedvencnek jelölte tülködet + %s megtolta a bejegyzésedet + %s kedvencnek jelölte a bejegyzésedet %s bekövetett \@%s jelentése Egyéb megjegyzés? @@ -100,7 +100,7 @@ Elutasítás Keresés Piszkozatok - Tülkök láthatósága + Bejegyzés láthatósága Tartalom figyelmeztetés Emoji billentyűzet Fül hozzáadása @@ -115,8 +115,8 @@ Link másolása Megnyitás mint %s Megosztás mint … - Tülk URL megosztása… - Tülk megosztása… + Bejegyzés URL megosztása… + Bejegyzés megosztása… Elküldve! Felhasználó letiltása feloldva Felhasználó némítása feloldva @@ -132,7 +132,7 @@ Válasz… Profilkép Fejléc - Mi az a szerver\? + Mi az a példány\? Csatlakozás… Bármely példány címét vagy domain nevét beírhatod ide, mint a mastodon.social, az icosahedron.website, a social.tchncs.de és mások! \n @@ -146,11 +146,11 @@ Letöltés Visszavonod a követési kérelmet? Követés megszüntetése? - Törlöd ezt a tülköt? - Nyilvános: Tülkölés nyilvános idővonalra + Törlöd ezt a bejegyzést\? + Nyilvános: Bejegyzés nyilvános idővonalra Listázatlan: Nem jelenik meg a nyilvános idővonalon - Csak követőknek: Tülkölés csak követőknek - Közvetlen: Tülkölés csak a megemlített felhasználóknak + Csak követőknek: Bejegyzés csak követőknek + Közvetlen: Bejegyzés csak a megemlített felhasználóknak Értesítések Értesítések Figyelmeztetések @@ -160,8 +160,8 @@ Értesítsen, ha megemlítettek bekövettek - tülkömet megtolták - tülkömet kedvenccé tették + bejegyzésemet megtolták + bejegyzésemet kedvencnek jelölték Megjelenés Idővonalak Sötét @@ -181,13 +181,13 @@ HTTP proxy engedélyezése HTTP proxy szerver HTTP Proxy port - Tülkök alapértelmezett láthatósága + Bejegyzések alapértelmezett láthatósága Minden média kényesnek jelölése A beállítások szinkronizálása nem sikerült Nyilvános Listázatlan Csak követőknek - Tülkölés szöveg mérete + Bejegyzés szövegének mérete Legkisebb Kicsi Közepes @@ -198,9 +198,9 @@ Új követők Értesítések új követőkről Megtolások - Értesítések tülkjeid megtolása esetén + Értesítések bejegyzéseid megtolása esetén Kedvencek - Értesítések mikor tülkjeidet kedvencnek jelölik + Értesítések amikor a bejegyzéseidet kedvencnek jelölik %s megemlített téged %1$s, %2$s, %3$s és még %4$d %1$s, %2$s meg %3$s @@ -224,10 +224,10 @@ Hibajelentés & új funkciók igénylése: \n https://github.com/tuskyapp/Tusky/issues Tusky profilja - Tülk tartalmának megosztása - Tülk linkjének megosztása + Bejegyzés tartalmának megosztása + Bejegyzés hivatkozásának megosztása Képek - Videók + Videó Követés kérelmezve Követ téged @@ -241,19 +241,19 @@ Törlés Fiók zárolása Elmented a piszkozatot\? - Tülk elküldése… - A tülk elküldése nem sikerült - Tülkök elküldése + Bejegyzés küldése… + A bejegyzés elküldése sikertelen + Bejegyzések elküldése Küldés megszakítva - A tülk másolatát elmentettük a piszkozataid közé + A bejegyzés másolatát elmentettük a piszkozataid közé Szerkesztés - A %s szervernek nincsenek egyedi emoji-jai + A %s példánynak nincsenek egyedi emoji-jai Vágólapra másolva Emoji stílus Rendszer alapértelmezés Először le kell töltened ezeket az emoji készleteket Keresés… - Tülk megnyitása + Bejegyzés megnyitása Az app újraindítása szükséges A beállítások érvényesítéséhez újra kell indítani a Tuskyt Később @@ -280,8 +280,7 @@ elérted a fülek maximális számát (%1$d) elérted a fülek maximális számát (%1$d) - Nincs leírás - + Nincs leírás Nyilvános Követők Kedvenc eltávolítása @@ -290,7 +289,7 @@ Média letöltése Média letöltése Média megosztása következővel… - Törlöd és újraírod ezt a tülköt\? + Törlöd és újraírod ezt a bejegyzést\? befejeződött egy szavazás Szűrők Rendszer téma használata @@ -342,7 +341,7 @@ Általad követettek keresése Fiók hozzáadása a listához Fiók eltávolítása a listából - Tülkölés %1$s fiókkal + Bejegyzés %1$s fiókkal Cím beállítása nem sikerült Leírás látássérülteknek @@ -350,7 +349,7 @@ Cím beállítása Minden követődet külön engedélyezned kell - Minden tülk kibontása/összecsukása + Összes bejegyzés kibontása/összecsukása A Google jelenlegi emodzsi készlete Megtolás az eredeti közönségnek Megtolás visszavonása @@ -368,7 +367,7 @@ %1$s %1$s, %2$s és még %3$d Média: %s - Tartalom figyelmeztetés: %s + Tartalomfigyelmeztetés: %s Megtolt Kedvelt Listázatlan @@ -379,7 +378,7 @@ Törlés Szűrés Alkalmaz - Tülk szerkesztése + Bejegyzés létrehozása Szerkesztés Biztos, hogy minden értesítésedet véglegesen törlöd\? Műveletek a(z) %s képpel @@ -416,11 +415,11 @@ Egyéb megjegyzések Továbbítás neki %s Nem sikerült a bejelentés - Nem sikerült a tülkök letöltése + Sikertelen a bejegyzések letöltése A bejelentést a szervered moderátorának küldjük el. Alább megadhatsz egy magyarázatot arra, hogy miért jelented be ezt a fiókot: A fiók egy másik szerverről származik. Küldjünk oda is egy anonimizált másolatot a bejelentésről\? Értesítések szűrőjének mutatása - Tartalom-figyelmeztetéssel ellátott tülkök kifejtése mindig + Tartalomfigyelmeztetéssel ellátott bejegyzések kinyitása mindig Fiókok Sikertelen keresés Szavazás hozzáadása @@ -436,12 +435,12 @@ Több lehetőség Válasz %d Szerkesztés - Időzített tülkök + Időzített bejegyzések Szerkesztés - Időzített tülkök - Tülk Időzítése + Időzített bejegyzések + Bejegyzés Időzítése Visszaállítás - Nem találjuk ezt a tülköt %s + Nem találjuk ezt a bejegyzést %s Könyvjelzők Könyvjelzőzés Könyvjelzők @@ -451,7 +450,7 @@ Lista A hangfájloknak kisebbnek kell lenniük, mint 40 MB. Nincs egy piszkozatod sem. - Nincs egy ütemezett tülköd sem. + Nincs egy ütemezett bejegyzésed sem. A Mastodonban a legrövidebb ütemezhető időintervallum 5 perc. Követési kérelmek Jóváhagyó ablak mutatása megtolás előtt @@ -484,34 +483,34 @@ Saját, mások számára nem látható megjegyzés erről a fiókról Nincsenek közlemények. Közlemények - A tülköt, melyre válaszul piszkozatot készítettél törölték + A bejegyzést, melyre válaszul piszkozatot készítettél törölték Piszkozat törölve Nem sikerült a Válasz információit betölteni - Ez a tülk nem küldődött el! + Ezt a bejegyzést nem tudtuk elküldeni! Tényleg le akarod törölni a %s listát\? Nem tölthetsz fel %1$d médiacsatolmányból többet. Nem tölthetsz fel %1$d médiacsatolmányból többet. Profilok mérőszámainak elrejtése - Tülkök mérőszámainak elrejtése + Bejegyzések mérőszámainak elrejtése Idővonali értesítések korlátozása Értesítések Áttekintése - Pár információ, ami befolyásolhatja a mentális egészségedet rejtve marad. Ilyenek pl.: + Pár információ, ami befolyásolhatja a mentális jóllétedet rejtve marad. Ilyenek pl.: \n \n - Kedvenc/Megtolás/Bekövetés értesítései -\n - Kedvenc/Megtolás számlálók a tülkökön -\n - Követő/Tülk statisztikák a profilokon +\n - Kedvenc/Megtolás számlálók a bejegyzéseken +\n - Követő/Bejegyzés statisztikák a profilokon \n \nA Push-értesítéseket ez nem befolyásolja, de kézzel átállíthatod az értesítési beállításaidat. Végtelen Időtartam Csatolmányok Audio - Értesítések általam követett személy új tülkjeiről - Új tülkök - valaki, akit követek újat tülkölt - %s épp tülkölt + Értesítések általam követett személy új bejegyzéseiről + Új bejegyzések + valaki, akit követek új bejegyzést tett közzé + %s épp bejegyzést írt Jóllét Egyedi emojik animálása Leiratkozás @@ -521,4 +520,19 @@ Beszélgetés törlése Könyvjelző törlése Jóváhagyás mutatása kedvencnek jelölés előtt + %s szerkesztette a bejegyzését + szerkesztették a bejegyzést, mellyel dolgod volt + %s regisztrált + valaki regisztrált + Regisztrációk + Értesítések új felhasználókról + 14 nap + 30 nap + 60 nap + 90 nap + 180 nap + 365 nap + Bejegyzések szerkesztése + Értesítések olyan bejegyzések szerkesztéséről, melyekkel már dolgod volt + Bejegyzés Létrehozása \ No newline at end of file From bdb53a333bf63ac46ee0f393a60056d2ad1ea980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Thu, 28 Apr 2022 16:13:28 +0000 Subject: [PATCH 032/104] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (477 of 477 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 16704821..0746ac6b 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -413,7 +413,7 @@ Tusky có sử dụng mã nguồn từ những dự án mã nguồn mở sau: Hủy đăng lại Đăng lại công khai - %1$s đã dời sang: + %1$s đã chuyển sang: Tài khoản Bot Tải về thất bại Emoji của Google From 04e51554543f630e7d6e31b9a107c11442d4e0e7 Mon Sep 17 00:00:00 2001 From: GunChleoc Date: Thu, 28 Apr 2022 16:13:28 +0000 Subject: [PATCH 033/104] Translated using Weblate (Gaelic) Currently translated at 100.0% (477 of 477 strings) Translated using Weblate (Gaelic) Currently translated at 100.0% (477 of 477 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 | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 19ae1240..eb80f729 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -249,16 +249,13 @@ A bheil thu airson a shàbhaladh ’na dhreachd\? Feumaidh tu gabhail ri luchd-leantainn ùr a làimh Glais an cunntas - Suidhidh am fo-thiotal + Suidhich am fo-thiotal Mìnich e dhan fheadhainn air a bheil cion-lèirsinn \n(%d caractar(an) air a char as fhaide) - Mìnich e dhan fheadhainn air a bheil cion-lèirsinn -\n(%d caractar(an) air a char as fhaide) - Mìnich e dhan fheadhainn air a bheil cion-lèirsinn -\n(%d caractar(an) air a char as fhaide) - Mìnich e dhan fheadhainn air a bheil cion-lèirsinn -\n(%d caractar(an) air a char as fhaide) + + + Cha deach leinn am fo-thiotal a shuidheachadh A’ postadh leis a’ chunntas %1$s @@ -552,4 +549,7 @@ Brathan mu cleachdaichean ùra chlàraich cuideigin Dheasaich %s am post aca + Deasachadh puist + Brathan nuair a thèid postaichean a rinn thu conaltradh leotha a dheasachadh + chaidh post a rinn mi conaltradh leis a deasachadh \ No newline at end of file From 3a11b9900e08ef89f74e6229b91be7550a9f5895 Mon Sep 17 00:00:00 2001 From: Constantin A <10349490+C1710@users.noreply.github.com> Date: Thu, 28 Apr 2022 18:55:10 +0200 Subject: [PATCH 034/104] EmojiCompat fix (#2468) * Add back the emojiInitCallback and move EmojiCompat init * Small adjustments * Make sure that we don't hit the IllegalStateException when EmojiCompat-ing the display names * Add a TODO for when Material Drawer 9 can be used * Remove EmojiCompat.process and initcallback --- app/src/main/java/com/keylesspalace/tusky/MainActivity.kt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 3e203059..466dba16 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -36,7 +36,6 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.view.GravityCompat -import androidx.emoji2.text.EmojiCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager @@ -810,11 +809,9 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje private fun updateProfiles() { val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) val profiles: MutableList = accountManager.getAllAccountsOrderedByActive().map { acc -> - val emojifiedName = EmojiCompat.get().process(acc.displayName.emojify(acc.emojis, header, animateEmojis))!! - ProfileDrawerItem().apply { isSelected = acc.isActive - nameText = emojifiedName + nameText = acc.displayName.emojify(acc.emojis, header, animateEmojis) iconUrl = acc.profilePictureUrl isNameShown = true identifier = acc.id From a3603c2154d475cb5c6d3d8b6f4d465e4ba140a6 Mon Sep 17 00:00:00 2001 From: codl Date: Thu, 28 Apr 2022 16:55:14 +0000 Subject: [PATCH 035/104] Translated using Weblate (French) Currently translated at 100.0% (477 of 477 strings) Co-authored-by: codl Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fr/ Translation: Tusky/Tusky --- app/src/main/res/values-fr/strings.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 0490c87b..248d4f0d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -544,4 +544,8 @@ Nouveaux comptes Notifications quand quelqu\'un crée un nouveau compte un nouveau compte a été créé + %s a modifié son message + un message avec lequel j\'ai interagi est modifié + Messages modifiés + Notifications quand un post avec lequel vous avez interagi est modifié \ No newline at end of file From 464f9d6412731029cf07bc53ea8cc85f63a9673b Mon Sep 17 00:00:00 2001 From: "Gera, Zoltan" Date: Thu, 28 Apr 2022 16:55:14 +0000 Subject: [PATCH 036/104] Translated using Weblate (Hungarian) Currently translated at 100.0% (477 of 477 strings) Translated using Weblate (Hungarian) Currently translated at 100.0% (477 of 477 strings) Co-authored-by: Gera, Zoltan Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/hu/ Translation: Tusky/Tusky --- app/src/main/res/values-hu/strings.xml | 128 ++++++++++++++----------- 1 file changed, 71 insertions(+), 57 deletions(-) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 17e4e788..18a4bb8a 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -9,26 +9,26 @@ Azonosítatlan engedélyezési hiba történt. Engedély megtagadva. Bejelentkezési token megszerzése sikertelen. - Túl hosszú a tülkölés! + 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. Média olvasási engedély szükséges. Média tárolási engedély szükséges. - Képek és videók egyszerre nem csatolhatók ugyanazon tülköléshez. + Képek és videók egyszerre nem csatolhatóak ugyanazon bejegyzéshez. Feltöltés sikertelen. - Nem sikerült elküldeni a tülköt. + Nem sikerült elküldeni a bejegyzést. Kezdőlap Értesítések Helyi Föderációs Közvetlen üzenetek Fülek - Tülk - Tülkök + Szál + Bejegyzések Válaszokkal - Rögzített + Kitűzött Követett Követő Kedvencek @@ -40,17 +40,17 @@ Licenszek \@%s %s megtolta - Kényes tartalom + Érzékeny tartalom Rejtett média - Kattints a megnézéshez + Kattints a megtekintéshez Mutass többet Mutass kevesebbet Kibontás Összecsukás Nincs itt semmi. Üres tartalom. Húzd le a frissítéshez! - %s megtolta a tülködet - %s kedvencnek jelölte tülködet + %s megtolta a bejegyzésedet + %s kedvencnek jelölte a bejegyzésedet %s bekövetett \@%s jelentése Egyéb megjegyzés? @@ -100,7 +100,7 @@ Elutasítás Keresés Piszkozatok - Tülkök láthatósága + Bejegyzés láthatósága Tartalom figyelmeztetés Emoji billentyűzet Fül hozzáadása @@ -115,8 +115,8 @@ Link másolása Megnyitás mint %s Megosztás mint … - Tülk URL megosztása… - Tülk megosztása… + Bejegyzés URL megosztása… + Bejegyzés megosztása… Elküldve! Felhasználó letiltása feloldva Felhasználó némítása feloldva @@ -132,7 +132,7 @@ Válasz… Profilkép Fejléc - Mi az a szerver\? + Mi az a példány\? Csatlakozás… Bármely példány címét vagy domain nevét beírhatod ide, mint a mastodon.social, az icosahedron.website, a social.tchncs.de és mások! \n @@ -146,11 +146,11 @@ Letöltés Visszavonod a követési kérelmet? Követés megszüntetése? - Törlöd ezt a tülköt? - Nyilvános: Tülkölés nyilvános idővonalra + Törlöd ezt a bejegyzést\? + Nyilvános: Bejegyzés nyilvános idővonalra Listázatlan: Nem jelenik meg a nyilvános idővonalon - Csak követőknek: Tülkölés csak követőknek - Közvetlen: Tülkölés csak a megemlített felhasználóknak + Csak követőknek: Bejegyzés csak követőknek + Közvetlen: Bejegyzés csak a megemlített felhasználóknak Értesítések Értesítések Figyelmeztetések @@ -160,8 +160,8 @@ Értesítsen, ha megemlítettek bekövettek - tülkömet megtolták - tülkömet kedvenccé tették + bejegyzésemet megtolták + bejegyzésemet kedvencnek jelölték Megjelenés Idővonalak Sötét @@ -181,13 +181,13 @@ HTTP proxy engedélyezése HTTP proxy szerver HTTP Proxy port - Tülkök alapértelmezett láthatósága + Bejegyzések alapértelmezett láthatósága Minden média kényesnek jelölése A beállítások szinkronizálása nem sikerült Nyilvános Listázatlan Csak követőknek - Tülkölés szöveg mérete + Bejegyzés szövegének mérete Legkisebb Kicsi Közepes @@ -198,9 +198,9 @@ Új követők Értesítések új követőkről Megtolások - Értesítések tülkjeid megtolása esetén + Értesítések bejegyzéseid megtolása esetén Kedvencek - Értesítések mikor tülkjeidet kedvencnek jelölik + Értesítések amikor a bejegyzéseidet kedvencnek jelölik %s megemlített téged %1$s, %2$s, %3$s és még %4$d %1$s, %2$s meg %3$s @@ -224,10 +224,10 @@ Hibajelentés & új funkciók igénylése: \n https://github.com/tuskyapp/Tusky/issues Tusky profilja - Tülk tartalmának megosztása - Tülk linkjének megosztása + Bejegyzés tartalmának megosztása + Bejegyzés hivatkozásának megosztása Képek - Videók + Videó Követés kérelmezve Követ téged @@ -241,19 +241,19 @@ Törlés Fiók zárolása Elmented a piszkozatot\? - Tülk elküldése… - A tülk elküldése nem sikerült - Tülkök elküldése + Bejegyzés küldése… + A bejegyzés elküldése sikertelen + Bejegyzések elküldése Küldés megszakítva - A tülk másolatát elmentettük a piszkozataid közé + A bejegyzés másolatát elmentettük a piszkozataid közé Szerkesztés - A %s szervernek nincsenek egyedi emoji-jai + A %s példánynak nincsenek egyedi emoji-jai Vágólapra másolva Emoji stílus Rendszer alapértelmezés Először le kell töltened ezeket az emoji készleteket Keresés… - Tülk megnyitása + Bejegyzés megnyitása Az app újraindítása szükséges A beállítások érvényesítéséhez újra kell indítani a Tuskyt Később @@ -280,8 +280,7 @@ elérted a fülek maximális számát (%1$d) elérted a fülek maximális számát (%1$d) - Nincs leírás - + Nincs leírás Nyilvános Követők Kedvenc eltávolítása @@ -290,7 +289,7 @@ Média letöltése Média letöltése Média megosztása következővel… - Törlöd és újraírod ezt a tülköt\? + Törlöd és újraírod ezt a bejegyzést\? befejeződött egy szavazás Szűrők Rendszer téma használata @@ -342,7 +341,7 @@ Általad követettek keresése Fiók hozzáadása a listához Fiók eltávolítása a listából - Tülkölés %1$s fiókkal + Bejegyzés %1$s fiókkal Cím beállítása nem sikerült Leírás látássérülteknek @@ -350,7 +349,7 @@ Cím beállítása Minden követődet külön engedélyezned kell - Minden tülk kibontása/összecsukása + Összes bejegyzés kibontása/összecsukása A Google jelenlegi emodzsi készlete Megtolás az eredeti közönségnek Megtolás visszavonása @@ -368,7 +367,7 @@ %1$s %1$s, %2$s és még %3$d Média: %s - Tartalom figyelmeztetés: %s + Tartalomfigyelmeztetés: %s Megtolt Kedvelt Listázatlan @@ -379,7 +378,7 @@ Törlés Szűrés Alkalmaz - Tülk szerkesztése + Bejegyzés létrehozása Szerkesztés Biztos, hogy minden értesítésedet véglegesen törlöd\? Műveletek a(z) %s képpel @@ -416,11 +415,11 @@ Egyéb megjegyzések Továbbítás neki %s Nem sikerült a bejelentés - Nem sikerült a tülkök letöltése + Sikertelen a bejegyzések letöltése A bejelentést a szervered moderátorának küldjük el. Alább megadhatsz egy magyarázatot arra, hogy miért jelented be ezt a fiókot: A fiók egy másik szerverről származik. Küldjünk oda is egy anonimizált másolatot a bejelentésről\? Értesítések szűrőjének mutatása - Tartalom-figyelmeztetéssel ellátott tülkök kifejtése mindig + Tartalomfigyelmeztetéssel ellátott bejegyzések kinyitása mindig Fiókok Sikertelen keresés Szavazás hozzáadása @@ -436,12 +435,12 @@ Több lehetőség Válasz %d Szerkesztés - Időzített tülkök + Időzített bejegyzések Szerkesztés - Időzített tülkök - Tülk Időzítése + Időzített bejegyzések + Bejegyzés Időzítése Visszaállítás - Nem találjuk ezt a tülköt %s + Nem találjuk ezt a bejegyzést %s Könyvjelzők Könyvjelzőzés Könyvjelzők @@ -451,7 +450,7 @@ Lista A hangfájloknak kisebbnek kell lenniük, mint 40 MB. Nincs egy piszkozatod sem. - Nincs egy ütemezett tülköd sem. + Nincs egy ütemezett bejegyzésed sem. A Mastodonban a legrövidebb ütemezhető időintervallum 5 perc. Követési kérelmek Jóváhagyó ablak mutatása megtolás előtt @@ -484,34 +483,34 @@ Saját, mások számára nem látható megjegyzés erről a fiókról Nincsenek közlemények. Közlemények - A tülköt, melyre válaszul piszkozatot készítettél törölték + A bejegyzést, melyre válaszul piszkozatot készítettél törölték Piszkozat törölve Nem sikerült a Válasz információit betölteni - Ez a tülk nem küldődött el! + Ezt a bejegyzést nem tudtuk elküldeni! Tényleg le akarod törölni a %s listát\? Nem tölthetsz fel %1$d médiacsatolmányból többet. Nem tölthetsz fel %1$d médiacsatolmányból többet. Profilok mérőszámainak elrejtése - Tülkök mérőszámainak elrejtése + Bejegyzések mérőszámainak elrejtése Idővonali értesítések korlátozása Értesítések Áttekintése - Pár információ, ami befolyásolhatja a mentális egészségedet rejtve marad. Ilyenek pl.: + Pár információ, ami befolyásolhatja a mentális jóllétedet rejtve marad. Ilyenek pl.: \n \n - Kedvenc/Megtolás/Bekövetés értesítései -\n - Kedvenc/Megtolás számlálók a tülkökön -\n - Követő/Tülk statisztikák a profilokon +\n - Kedvenc/Megtolás számlálók a bejegyzéseken +\n - Követő/Bejegyzés statisztikák a profilokon \n \nA Push-értesítéseket ez nem befolyásolja, de kézzel átállíthatod az értesítési beállításaidat. Végtelen Időtartam Csatolmányok Audio - Értesítések általam követett személy új tülkjeiről - Új tülkök - valaki, akit követek újat tülkölt - %s épp tülkölt + Értesítések általam követett személy új bejegyzéseiről + Új bejegyzések + valaki, akit követek új bejegyzést tett közzé + %s épp bejegyzést írt Jóllét Egyedi emojik animálása Leiratkozás @@ -521,4 +520,19 @@ Beszélgetés törlése Könyvjelző törlése Jóváhagyás mutatása kedvencnek jelölés előtt + %s szerkesztette a bejegyzését + szerkesztették a bejegyzést, mellyel dolgod volt + %s regisztrált + valaki regisztrált + Regisztrációk + Értesítések új felhasználókról + 14 nap + 30 nap + 60 nap + 90 nap + 180 nap + 365 nap + Bejegyzések szerkesztése + Értesítések olyan bejegyzések szerkesztéséről, melyekkel már dolgod volt + Bejegyzés Létrehozása \ No newline at end of file From 384236f832d3dd0303d57240195ffcfce13858b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Thu, 28 Apr 2022 16:55:14 +0000 Subject: [PATCH 037/104] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (477 of 477 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 16704821..0746ac6b 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -413,7 +413,7 @@ Tusky có sử dụng mã nguồn từ những dự án mã nguồn mở sau: Hủy đăng lại Đăng lại công khai - %1$s đã dời sang: + %1$s đã chuyển sang: Tài khoản Bot Tải về thất bại Emoji của Google From b94d723663e600ae8d0a74f7eba1d1a314291a2c Mon Sep 17 00:00:00 2001 From: GunChleoc Date: Thu, 28 Apr 2022 16:55:14 +0000 Subject: [PATCH 038/104] Translated using Weblate (Gaelic) Currently translated at 100.0% (477 of 477 strings) Translated using Weblate (Gaelic) Currently translated at 100.0% (477 of 477 strings) Translated using Weblate (Gaelic) Currently translated at 100.0% (477 of 477 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 | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 19ae1240..cc46053b 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -249,16 +249,16 @@ A bheil thu airson a shàbhaladh ’na dhreachd\? Feumaidh tu gabhail ri luchd-leantainn ùr a làimh Glais an cunntas - Suidhidh am fo-thiotal + Suidhich am fo-thiotal Mìnich e dhan fheadhainn air a bheil cion-lèirsinn -\n(%d caractar(an) air a char as fhaide) +\n(%d charactar air a char as fhaide) Mìnich e dhan fheadhainn air a bheil cion-lèirsinn -\n(%d caractar(an) air a char as fhaide) +\n(%d charactar air a char as fhaide) Mìnich e dhan fheadhainn air a bheil cion-lèirsinn -\n(%d caractar(an) air a char as fhaide) +\n(%d caractaran air a char as fhaide) Mìnich e dhan fheadhainn air a bheil cion-lèirsinn -\n(%d caractar(an) air a char as fhaide) +\n(%d caractar air a char as fhaide) Cha deach leinn am fo-thiotal a shuidheachadh A’ postadh leis a’ chunntas %1$s @@ -552,4 +552,7 @@ Brathan mu cleachdaichean ùra chlàraich cuideigin Dheasaich %s am post aca + Deasachadh puist + Brathan nuair a thèid postaichean a rinn thu conaltradh leotha a dheasachadh + chaidh post a rinn mi conaltradh leis a deasachadh \ No newline at end of file From f7599db777f9ea1de6a514a4b77de02d287f841c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Thu, 28 Apr 2022 16:55:14 +0000 Subject: [PATCH 039/104] Translated using Weblate (Icelandic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (477 of 477 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 0dc453c5..46948bf0 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -519,4 +519,12 @@ 365 dagar 14 dagar Semja færslu + %s skráði sig + einhver skráði sig + %s breytti færslunni sinni + færsla sem ég hef átt við er breytt + Nýskráningar + Tilkynningar um nýja notendur + Breytingar á færslum + Tilkynningar þegar færslum sem þú hefur átt við er breytt \ No newline at end of file From 516f75b643bcb3b804b13ea072ec76235c08a5b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20=C4=BDach?= Date: Thu, 28 Apr 2022 16:55:14 +0000 Subject: [PATCH 040/104] Translated using Weblate (Slovak) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 26.4% (126 of 477 strings) Co-authored-by: Marek Ľach Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/sk/ Translation: Tusky/Tusky --- app/src/main/res/values-sk/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 15d05945..e06555c7 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -77,7 +77,7 @@ Autentizácia servru zlyhala. Nepodarilo sa nájsť použiteľný webový prehliadač. Vyskytla sa neidentifikovaná chyba autorizácie. - Toot je príliš dlhý! + Príspevok je príliš dlhý! Tento typ súboru nemôže byť nahraný. Chyba pri odosielaní tootu. Toot From 895130bd4b620100f18187eec902f2130dbbf84b Mon Sep 17 00:00:00 2001 From: Roj Date: Thu, 28 Apr 2022 16:55:15 +0000 Subject: [PATCH 041/104] Translated using Weblate (Sorani) Currently translated at 88.6% (423 of 477 strings) Co-authored-by: Roj Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ckb/ Translation: Tusky/Tusky --- app/src/main/res/values-ckb/strings.xml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index 8dd86758..ad91f902 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -123,11 +123,11 @@ نیشانەکان دڵخوازەکان شوێنکەوتوان - بەدوادا + شوێنکەوتنەکان چەسپا لەگەڵ وەڵامەکان بابەتەکان - توت + زنجیرە سەرخشتەکان نامە ڕاستەوخۆکان گشتی @@ -140,19 +140,19 @@ مۆڵەت بۆ پاشکەوتکردنی میدیا پێویستە. مۆڵەت بۆ خوێندنەوەی میدیا پێویستە. ئەم فایلە ناتوانرێت بکرێتەوە. - ناتوانرێت ئەو جۆرە فایلە باربکرێت. - فایلەکانی دەنگ دەبێت کەمتر بێت لە ٤٠MB. - پێویستە فایلەکانی ڤیدیۆ کەمتر لە 40 مێگابایت بن. - فایلەکە دەبێت کەمتر بێت لە 8 مێگابایت. - ڕەستە زۆر درێژە! + ناتوانیت لەم جۆرە فایلانە بەرز بکەیتەوە. + دەبێت فایلە دەنگییەکان لە 40 مێگابایت گەورەتر نەبن. + دەبێت ڤیدیۆکان لە 40 مێگابایت گەورەتر نەبن. + فایلەکە دەبێت لە 8 مێگابایت بچووکتر بێت. + ئەم نووسینە زۆر درێژە! سەرکەوتوو نەبوو لە بەدەستهێنانی نیشانەی چوونەژوورەوە. ڕێپێدان ڕەتکرایەوە. هەڵەیەک بۆ مۆڵەتدانی نەناسراو ڕووی دا. نەیتوانی وێبگەڕبدۆزێتەوە بۆ بەکارهێنان. سەرکەوتوو نەبوو، ڕاستکردنەوە لەگەڵ ئەم نمونەیە. - دۆمەینی نادروست تێنووسکرا - ئەمە ناتوانێت بەتاڵ بێت. - هەڵەیەک لە تۆڕ ڕوویدا! تکایە پەیوەندیت بپشکنە و دوبارە هەوڵ بدە! + دۆمەینێکی نادروستت نووسیوە + ناکرێت ئەمە بەتاڵ بێت. + هەڵەیەک لە پەیوەندییەکەدا ڕوویدا. تکایە دڵنیا ببەوە لە بەردەستبوونی هێڵی ئینتەرنێت. هەڵەیەک ڕوویدا. تایبەتمەندی بابەت گریمانەیی دەرگای پرۆکسی HTTP From 0fd51dbcec2730546da12326a3b9745916607467 Mon Sep 17 00:00:00 2001 From: "Gera, Zoltan" Date: Sat, 23 Apr 2022 11:40:29 +0000 Subject: [PATCH 042/104] Translated using Weblate (Hungarian) Currently translated at 100.0% (16 of 16 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/hu/ --- fastlane/metadata/android/hu/changelogs/89.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 fastlane/metadata/android/hu/changelogs/89.txt diff --git a/fastlane/metadata/android/hu/changelogs/89.txt b/fastlane/metadata/android/hu/changelogs/89.txt new file mode 100644 index 00000000..e7803769 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Megnyit, mint..." már a fiókok profiljainak menüjében is elérhető, amikor több fiókot használsz +- A bejelentkezés az appon belül már WebView-ban működik +- Android 12 támogatása +- új Mastodon szerverkonfigurációs API támogatása +- sok más kisebb javítás és fejlesztés From 671d2c6a45cc4b286f5382a1cb22e2c92982a851 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 28 Apr 2022 20:37:31 +0200 Subject: [PATCH 043/104] Check if media processing finished before sending status (#2458) * make MastodonApi.createStatus suspending * check if media processing has finished before sending status * add backoff for retrying processed media check --- .../components/compose/ComposeViewModel.kt | 11 +- .../tusky/network/MastodonApi.kt | 9 +- .../receiver/SendStatusBroadcastReceiver.kt | 3 +- .../tusky/service/SendStatusService.kt | 163 ++++++++++-------- 4 files changed, 103 insertions(+), 83 deletions(-) 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 fce3d0bd..81500ee4 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 @@ -251,13 +251,15 @@ class ComposeViewModel @Inject constructor( val sendObservable = media .filter { items -> items.all { it.uploadPercent == -1 } } .map { - val mediaIds = ArrayList() - val mediaUris = ArrayList() - val mediaDescriptions = ArrayList() + val mediaIds: MutableList = mutableListOf() + val mediaUris: MutableList = mutableListOf() + val mediaDescriptions: MutableList = mutableListOf() + val mediaProcessed: MutableList = mutableListOf() for (item in media.value!!) { mediaIds.add(item.id!!) mediaUris.add(item.uri) mediaDescriptions.add(item.description ?: "") + mediaProcessed.add(false) } val tootToSend = StatusToSend( @@ -276,7 +278,8 @@ class ComposeViewModel @Inject constructor( accountId = accountManager.activeAccount!!.id, draftId = draftId, idempotencyKey = randomAlphanumericString(16), - retries = 0 + retries = 0, + mediaProcessed = mediaProcessed ) serviceClient.sendToot(tootToSend) 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 02af5caa..25fe03d4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -156,13 +156,18 @@ interface MastodonApi { @Field("description") description: String ): Result + @GET("api/v1/media/{mediaId}") + suspend fun getMedia( + @Path("mediaId") mediaId: String + ): Response + @POST("api/v1/statuses") - fun createStatus( + suspend fun createStatus( @Header("Authorization") auth: String, @Header(DOMAIN_HEADER) domain: String, @Header("Idempotency-Key") idempotencyKey: String, @Body status: NewStatus - ): Call + ): Result @GET("api/v1/statuses/{id}") fun status( 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 c6df5df6..a0eac833 100644 --- a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -100,7 +100,8 @@ class SendStatusBroadcastReceiver : BroadcastReceiver() { accountId = account.id, draftId = -1, idempotencyKey = randomAlphanumericString(16), - retries = 0 + retries = 0, + mediaProcessed = mutableListOf() ) ) 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 a2709f97..e50f4f4f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt @@ -11,6 +11,7 @@ import android.content.Intent import android.os.Build import android.os.IBinder import android.os.Parcelable +import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat @@ -29,13 +30,12 @@ import com.keylesspalace.tusky.network.MastodonApi import dagger.android.AndroidInjection import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize -import retrofit2.Call -import retrofit2.Callback -import retrofit2.Response +import retrofit2.HttpException import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -55,7 +55,7 @@ class SendStatusService : Service(), Injectable { private val serviceScope = CoroutineScope(Dispatchers.Main + supervisorJob) private val statusesToSend = ConcurrentHashMap() - private val sendCalls = ConcurrentHashMap>() + private val sendJobs = ConcurrentHashMap() private val notificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } @@ -64,12 +64,9 @@ class SendStatusService : Service(), Injectable { super.onCreate() } - override fun onBind(intent: Intent): IBinder? { - return null - } + override fun onBind(intent: Intent): IBinder? = null override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - if (intent.hasExtra(KEY_STATUS)) { val statusToSend = intent.getParcelableExtra(KEY_STATUS) ?: throw IllegalStateException("SendStatusService started without $KEY_STATUS extra") @@ -129,82 +126,94 @@ class SendStatusService : Service(), Injectable { statusToSend.retries++ - val newStatus = NewStatus( - statusToSend.text, - statusToSend.warningText, - statusToSend.inReplyToId, - statusToSend.visibility, - statusToSend.sensitive, - statusToSend.mediaIds, - statusToSend.scheduledAt, - statusToSend.poll - ) + sendJobs[statusId] = serviceScope.launch { + try { + var mediaCheckRetries = 0 + while (statusToSend.mediaProcessed.any { !it }) { + delay(1000L * mediaCheckRetries) + statusToSend.mediaProcessed.forEachIndexed { index, processed -> + if (!processed) { + // Mastodon returns 206 if the media was not yet processed + statusToSend.mediaProcessed[index] = mastodonApi.getMedia(statusToSend.mediaIds[index]).code() == 200 + } + } + mediaCheckRetries ++ + } + } catch (e: Exception) { + Log.w(TAG, "failed getting media status", e) + retrySending(statusId) + return@launch + } - val sendCall = mastodonApi.createStatus( - "Bearer " + account.accessToken, - account.domain, - statusToSend.idempotencyKey, - newStatus - ) + val newStatus = NewStatus( + statusToSend.text, + statusToSend.warningText, + statusToSend.inReplyToId, + statusToSend.visibility, + statusToSend.sensitive, + statusToSend.mediaIds, + statusToSend.scheduledAt, + statusToSend.poll + ) - sendCalls[statusId] = sendCall + mastodonApi.createStatus( + "Bearer " + account.accessToken, + account.domain, + statusToSend.idempotencyKey, + newStatus + ).fold({ sentStatus -> + statusesToSend.remove(statusId) + // If the status was loaded from a draft, delete the draft and associated media files. + if (statusToSend.draftId != 0) { + draftHelper.deleteDraftAndAttachments(statusToSend.draftId) + } - val callback = object : Callback { - override fun onResponse(call: Call, response: Response) { - serviceScope.launch { + val scheduled = !statusToSend.scheduledAt.isNullOrEmpty() - val scheduled = !statusToSend.scheduledAt.isNullOrEmpty() + if (scheduled) { + eventHub.dispatch(StatusScheduledEvent(sentStatus)) + } else { + eventHub.dispatch(StatusComposedEvent(sentStatus)) + } + + notificationManager.cancel(statusId) + }, { throwable -> + Log.w(TAG, "failed sending status", throwable) + if (throwable is HttpException) { + // the server refused to accept the status, save status & show error message statusesToSend.remove(statusId) + saveStatusToDrafts(statusToSend) - if (response.isSuccessful) { - // If the status was loaded from a draft, delete the draft and associated media files. - if (statusToSend.draftId != 0) { - draftHelper.deleteDraftAndAttachments(statusToSend.draftId) - } - - if (scheduled) { - response.body()?.let(::StatusScheduledEvent)?.let(eventHub::dispatch) - } else { - response.body()?.let(::StatusComposedEvent)?.let(eventHub::dispatch) - } - - notificationManager.cancel(statusId) - } else { - // the server refused to accept the status, save status & show error message - 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( - ContextCompat.getColor( - this@SendStatusService, - R.color.notification_color - ) + 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( + ContextCompat.getColor( + this@SendStatusService, + R.color.notification_color ) + ) - notificationManager.cancel(statusId) - notificationManager.notify(errorNotificationId--, builder.build()) - } - stopSelfWhenDone() + notificationManager.cancel(statusId) + notificationManager.notify(errorNotificationId--, builder.build()) + } else { + // a network problem occurred, let's retry sending the status + retrySending(statusId) } - } - - override fun onFailure(call: Call, t: Throwable) { - serviceScope.launch { - var backoff = TimeUnit.SECONDS.toMillis(statusToSend.retries.toLong()) - if (backoff > MAX_RETRY_INTERVAL) { - backoff = MAX_RETRY_INTERVAL - } - - delay(backoff) - sendStatus(statusId) - } - } + }) + stopSelfWhenDone() } + } - sendCall.enqueue(callback) + private suspend fun retrySending(statusId: Int) { + // when statusToSend == null, sending has been canceled + val statusToSend = statusesToSend[statusId] ?: return + + val backoff = TimeUnit.SECONDS.toMillis(statusToSend.retries.toLong()).coerceAtMost(MAX_RETRY_INTERVAL) + + delay(backoff) + sendStatus(statusId) } private fun stopSelfWhenDone() { @@ -218,8 +227,8 @@ class SendStatusService : Service(), Injectable { private fun cancelSending(statusId: Int) = serviceScope.launch { val statusToCancel = statusesToSend.remove(statusId) if (statusToCancel != null) { - val sendCall = sendCalls.remove(statusId) - sendCall?.cancel() + val sendJob = sendJobs.remove(statusId) + sendJob?.cancel() saveStatusToDrafts(statusToCancel) @@ -263,6 +272,7 @@ class SendStatusService : Service(), Injectable { } companion object { + private const val TAG = "SendStatusService" private const val KEY_STATUS = "status" private const val KEY_CANCEL = "cancel_id" @@ -319,5 +329,6 @@ data class StatusToSend( val accountId: Long, val draftId: Int, val idempotencyKey: String, - var retries: Int + var retries: Int, + val mediaProcessed: MutableList ) : Parcelable From 28ac190212ad989b1d53372957fe741299b9068a Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 28 Apr 2022 20:37:46 +0200 Subject: [PATCH 044/104] fix SearchActivity transition animations (#2464) --- .../tusky/components/search/SearchActivity.kt | 4 ++++ .../tusky/components/search/fragments/SearchFragment.kt | 8 ++++++-- .../components/search/fragments/SearchStatusesFragment.kt | 2 +- .../tusky/interfaces/StatusActionListener.java | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt index d833b432..8ca7248c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt @@ -84,6 +84,10 @@ class SearchActivity : BottomSheetActivity(), HasAndroidInjector { return true } + override fun finish() { + super.finishWithoutSlideOutAnimation() + } + private fun getPageTitle(position: Int): CharSequence { return when (position) { 0 -> getString(R.string.title_posts) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt index aeb989b0..a7707491 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt @@ -111,9 +111,13 @@ abstract class SearchFragment : } } - override fun onViewAccount(id: String) = startActivity(AccountActivity.getIntent(requireContext(), id)) + override fun onViewAccount(id: String) { + bottomSheetActivity?.startActivityWithSlideInAnimation(AccountActivity.getIntent(requireContext(), id)) + } - override fun onViewTag(tag: String) = startActivity(StatusListActivity.newHashtagIntent(requireContext(), tag)) + override fun onViewTag(tag: String) { + bottomSheetActivity?.startActivityWithSlideInAnimation(StatusListActivity.newHashtagIntent(requireContext(), tag)) + } override fun onViewUrl(url: String) { bottomSheetActivity?.viewUrl(url) 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 11d90da4..2e7849c1 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 @@ -219,7 +219,7 @@ class SearchStatusesFragment : SearchFragment(), Status replyingStatusContent = status.content.toString() ) ) - startActivity(intent) + bottomSheetActivity?.startActivityWithSlideInAnimation(intent) } private fun more(status: Status, view: View, position: Int) { diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java index 116e582c..ec37680c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java @@ -38,7 +38,7 @@ public interface StatusActionListener extends LinkListener { void onOpenReblog(int position); void onExpandedChange(boolean expanded, int position); void onContentHiddenChange(boolean isShowing, int position); - void onLoadMore(int position); + void onLoadMore(int position); /** * Called when the status {@link android.widget.ToggleButton} responsible for collapsing long From e5b58770ced893cb24733efe42425d4fb7bcdda8 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 28 Apr 2022 20:38:51 +0200 Subject: [PATCH 045/104] improve LoginWebViewActivityUX (#2465) --- .../components/login/LoginWebViewActivity.kt | 18 +++++++++++++++--- app/src/main/res/layout/login_webview.xml | 6 ++++++ app/src/main/res/values/strings.xml | 1 + 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt index 827b5620..58f745e7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt @@ -19,8 +19,10 @@ import androidx.activity.result.contract.ActivityResultContract import androidx.core.net.toUri import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.LoginWebviewBinding import com.keylesspalace.tusky.di.Injectable +import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.viewBinding import kotlinx.parcelize.Parcelize @@ -87,7 +89,9 @@ class LoginWebViewActivity : BaseActivity(), Injectable { setSupportActionBar(binding.loginToolbar) supportActionBar?.setDisplayHomeAsUpEnabled(true) - supportActionBar?.setDisplayShowTitleEnabled(false) + supportActionBar?.setDisplayShowTitleEnabled(true) + + setTitle(R.string.title_login) val webView = binding.loginWebView webView.settings.allowContentAccess = false @@ -103,13 +107,17 @@ class LoginWebViewActivity : BaseActivity(), Injectable { val oauthUrl = data.oauthRedirectUrl webView.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + binding.loginProgress.hide() + } + override fun onReceivedError( view: WebView, request: WebResourceRequest, error: WebResourceError ) { Log.d("LoginWeb", "Failed to load ${data.url}: $error") - finish() + finishWithoutSlideOutAnimation() } override fun shouldOverrideUrlLoading( @@ -165,10 +173,14 @@ class LoginWebViewActivity : BaseActivity(), Injectable { super.onDestroy() } + override fun finish() { + super.finishWithoutSlideOutAnimation() + } + override fun requiresLogin() = false private fun sendResult(result: LoginResult) { setResult(Activity.RESULT_OK, OauthLogin.makeResultIntent(result)) - finish() + finishWithoutSlideOutAnimation() } } diff --git a/app/src/main/res/layout/login_webview.xml b/app/src/main/res/layout/login_webview.xml index 22d7f3bd..67d47d6f 100644 --- a/app/src/main/res/layout/login_webview.xml +++ b/app/src/main/res/layout/login_webview.xml @@ -16,6 +16,12 @@ + + The upload failed. Error sending post. + Login Home Notifications Local From e9b75119b3f02382327e66c4fb66bac3bd17409d Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 28 Apr 2022 20:39:06 +0200 Subject: [PATCH 046/104] improve bot badge (#2466) * improve bot badge * change badge corner radius --- .../tusky/adapter/AccountViewHolder.java | 3 +-- .../tusky/adapter/NotificationsAdapter.java | 3 +-- .../tusky/adapter/StatusBaseViewHolder.java | 7 ++++--- app/src/main/res/drawable/bot_badge.xml | 13 +++++++++++++ app/src/main/res/values-night/theme_colors.xml | 3 +++ app/src/main/res/values/theme_colors.xml | 3 +++ 6 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 app/src/main/res/drawable/bot_badge.xml diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java index f4824389..6672fff3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.java @@ -45,8 +45,7 @@ public class AccountViewHolder extends RecyclerView.ViewHolder { ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar); if (showBotOverlay && account.getBot()) { avatarInset.setVisibility(View.VISIBLE); - avatarInset.setImageResource(R.drawable.ic_bot_24dp); - avatarInset.setBackgroundColor(0x50ffffff); + avatarInset.setImageResource(R.drawable.bot_badge); } else { avatarInset.setVisibility(View.GONE); } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index f8885f36..f681e64f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -575,9 +575,8 @@ public class NotificationsAdapter extends RecyclerView.Adapter { if (statusDisplayOptions.showBotOverlay() && isBot) { notificationAvatar.setVisibility(View.VISIBLE); - notificationAvatar.setBackgroundColor(0x50ffffff); Glide.with(notificationAvatar) - .load(R.drawable.ic_bot_24dp) + .load(ContextCompat.getDrawable(notificationAvatar.getContext(), R.drawable.bot_badge)) .into(notificationAvatar); } else { 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 c7427450..2a5b3f2c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -21,6 +21,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.content.ContextCompat; import androidx.core.text.HtmlCompat; import androidx.recyclerview.widget.DefaultItemAnimator; import androidx.recyclerview.widget.LinearLayoutManager; @@ -28,6 +29,7 @@ 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; @@ -287,11 +289,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { if (statusDisplayOptions.showBotOverlay() && isBot) { avatarInset.setVisibility(View.VISIBLE); - avatarInset.setBackgroundColor(0x50ffffff); Glide.with(avatarInset) - .load(R.drawable.ic_bot_24dp) + // passing the drawable id directly into .load() ignores night mode https://github.com/bumptech/glide/issues/4692 + .load(ContextCompat.getDrawable(avatarInset.getContext(), R.drawable.bot_badge)) .into(avatarInset); - } else { avatarInset.setVisibility(View.GONE); } diff --git a/app/src/main/res/drawable/bot_badge.xml b/app/src/main/res/drawable/bot_badge.xml new file mode 100644 index 00000000..6f857df5 --- /dev/null +++ b/app/src/main/res/drawable/bot_badge.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/values-night/theme_colors.xml b/app/src/main/res/values-night/theme_colors.xml index e45c45fb..21e56190 100644 --- a/app/src/main/res/values-night/theme_colors.xml +++ b/app/src/main/res/values-night/theme_colors.xml @@ -24,4 +24,7 @@ false + @color/white + @color/tusky_grey_10 + \ No newline at end of file diff --git a/app/src/main/res/values/theme_colors.xml b/app/src/main/res/values/theme_colors.xml index bf5b79fc..657363d8 100644 --- a/app/src/main/res/values/theme_colors.xml +++ b/app/src/main/res/values/theme_colors.xml @@ -24,4 +24,7 @@ true + @color/tusky_grey_20 + @color/white + \ No newline at end of file From 9a8dfaa744ee0344a75aa2cfa381fa4c2c503206 Mon Sep 17 00:00:00 2001 From: Guntbert Reiter Date: Fri, 29 Apr 2022 18:35:25 +0200 Subject: [PATCH 047/104] Remove code to check the server version (#2469) solves #2439 --- .../components/compose/ComposeActivity.kt | 1 - .../components/instanceinfo/InstanceInfo.kt | 3 +- .../instanceinfo/InstanceInfoRepository.kt | 4 +- .../tusky/util/VersionUtils.java | 44 ------------------- .../tusky/util/VersionUtilsTest.kt | 35 --------------- 5 files changed, 2 insertions(+), 85 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/util/VersionUtils.java delete mode 100644 app/src/test/java/com/keylesspalace/tusky/util/VersionUtilsTest.kt 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 b7a65a7f..48e14def 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 @@ -335,7 +335,6 @@ class ComposeActivity : maximumTootCharacters = instanceData.maxChars charactersReservedPerUrl = instanceData.charactersReservedPerUrl updateVisibleCharactersLeft() - binding.composeScheduleButton.visible(instanceData.supportsScheduled) } viewModel.emoji.observe { emoji -> setEmojiList(emoji) } combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning -> 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 db6ec0e1..05e10b6b 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,6 +21,5 @@ data class InstanceInfo( val pollMaxLength: Int, val pollMinDuration: Int, val pollMaxDuration: Int, - val charactersReservedPerUrl: Int, - val supportsScheduled: Boolean + val charactersReservedPerUrl: 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 287d5499..8ed26d7b 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 @@ -22,7 +22,6 @@ import com.keylesspalace.tusky.db.EmojisEntity import com.keylesspalace.tusky.db.InstanceInfoEntity import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.util.VersionUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import javax.inject.Inject @@ -83,8 +82,7 @@ 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, - supportsScheduled = instanceInfo?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false + charactersReservedPerUrl = instanceInfo?.charactersReservedPerUrl ?: DEFAULT_CHARACTERS_RESERVED_PER_URL ) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/VersionUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/VersionUtils.java deleted file mode 100644 index dceef0f3..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/VersionUtils.java +++ /dev/null @@ -1,44 +0,0 @@ -/* Copyright 2019 kyori19 - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.util; - -import androidx.annotation.NonNull; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class VersionUtils { - - private int major; - private int minor; - private int patch; - - public VersionUtils(@NonNull String versionString) { - String regex = "([0-9]+)\\.([0-9]+)\\.([0-9]+).*"; - Pattern pattern = Pattern.compile(regex); - Matcher matcher = pattern.matcher(versionString); - if (matcher.find()) { - major = Integer.parseInt(matcher.group(1)); - minor = Integer.parseInt(matcher.group(2)); - patch = Integer.parseInt(matcher.group(3)); - } - } - - public boolean supportsScheduledToots() { - return (major == 2) ? ( (minor == 7) ? (patch >= 0) : (minor > 7) ) : (major > 2); - } - -} diff --git a/app/src/test/java/com/keylesspalace/tusky/util/VersionUtilsTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/VersionUtilsTest.kt deleted file mode 100644 index 2731228a..00000000 --- a/app/src/test/java/com/keylesspalace/tusky/util/VersionUtilsTest.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.keylesspalace.tusky.util - -import org.junit.Assert.assertEquals -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized - -@RunWith(Parameterized::class) -class VersionUtilsTest( - private val versionString: String, - private val supportsScheduledToots: Boolean -) { - - companion object { - @JvmStatic - @Parameterized.Parameters - fun data() = listOf( - arrayOf("2.0.0", false), - arrayOf("2a9a0", false), - arrayOf("1.0", false), - arrayOf("error", false), - arrayOf("", false), - arrayOf("2.6.9", false), - arrayOf("2.7.0", true), - arrayOf("2.00008.0", true), - arrayOf("2.7.2 (compatible; Pleroma 1.0.0-1168-ge18c7866-pleroma-dot-site)", true), - arrayOf("3.0.1", true) - ) - } - - @Test - fun testVersionUtils() { - assertEquals(VersionUtils(versionString).supportsScheduledToots(), supportsScheduledToots) - } -} From 7fd54e3b4f4c03d7e2b898a1e57fb7ce7c94c5ff Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sat, 30 Apr 2022 08:09:59 +0200 Subject: [PATCH 048/104] fix timeline refresh spinner finishing before updates are visible --- .../keylesspalace/tusky/components/timeline/TimelineFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt index f9175052..54183888 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt @@ -172,7 +172,7 @@ class TimelineFragment : setupRecyclerView() adapter.addLoadStateListener { loadState -> - if (loadState.refresh != LoadState.Loading) { + if (loadState.refresh != LoadState.Loading && loadState.source.refresh != LoadState.Loading) { binding.swipeRefreshLayout.isRefreshing = false } From 0b46e6bc35d6f6bbe5168e0cedfff90fe05b3752 Mon Sep 17 00:00:00 2001 From: Luna Date: Sat, 30 Apr 2022 09:17:54 +0000 Subject: [PATCH 049/104] Translated using Weblate (Polish) Currently translated at 100.0% (478 of 478 strings) Co-authored-by: Luna Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pl/ Translation: Tusky/Tusky --- app/src/main/res/values-pl/strings.xml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d468ee00..eb1bf573 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -519,7 +519,7 @@ ktoś kogo zasubskrybowałem/zasubskrybowałam opublikował nowy wpis Wysłano prośbę o obserwowanie Ogłoszenia - Samopoczucie + Zdrowie Anuluj subskrypcję Zasubskrybuj Mimo tego, że twoje konto nie jest zablokowane, administracja %1$s uznała, że możesz chcieć ręcznie przejrzeć te prośby o możliwość śledzenia od tych kont. @@ -530,7 +530,7 @@ Przejrzyj powiadomienia Zapisano! Twoja prywatna notatka o tym koncie - Czas nieokreślony + Nieskończona Dźwięk Powiadomienia o opublikowaniu nowego wpisu przez kogoś, kogo obserwujesz Pozycja głównego paska nawigacji @@ -549,4 +549,13 @@ 180 dni 365 dni Utwórz wpis + Login + %s zarejestrował(a) się + Rejestracje + Powiadomienia o nowych użytkownikach + Powiadomienia o edycji wpisów z którymi interaktowałeś/aś + ktoś zarejestrował się + wpis, z którym interaktowałem/am został edytowany + %s edytował(a) swój wpis + Edycje wpisów \ No newline at end of file From ddf3a5992baa8b1ecca4b1b784c6ceb037e64909 Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 30 Apr 2022 09:17:55 +0000 Subject: [PATCH 050/104] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (478 of 478 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 | 39 ++++++++++++---------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index c2fd66d4..6467706a 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -27,7 +27,7 @@ 标签页 嘟文 嘟文 - 嘟文和回复 + 有回复 已置顶 正在关注 关注者 @@ -42,7 +42,7 @@ %s 转嘟了 敏感内容 已隐藏的照片或视频 - 点击显示 + 点击查看 显示更多 折叠内容 展开 @@ -156,7 +156,7 @@ 移除关注请求? 不再关注此用户? 删除这条嘟文? - 删除并重新编辑这条嘟文? + 删除并重新起草这条嘟文? 公开:所有人可见,并会出现在公共时间轴上 不公开:所有人可见,但不会出现在公共时间轴上 仅关注者:只有经过你确认后关注你的用户可见 @@ -203,7 +203,7 @@ 公开 不公开 仅关注者 - 字体大小 + 嘟文字体大小 最小 标准 @@ -299,11 +299,11 @@ 保护你的帐户(锁嘟) 你需要手动审核所有关注请求 保存为草稿? - 正在发送… - 发送失败 + 正在发送嘟文… + 嘟文发送出错 嘟文发送中 已取消发送 - 嘟文已保存为草稿 + 嘟文副本已保存为草稿 发表嘟文 当前实例 %s 没有自定义表情符号 已复制到剪贴板 @@ -353,16 +353,10 @@ 标签页不能超过 %1$d 个 媒体:%s - 内容提醒:%s - - 没有媒体描述信息 - - - 被转嘟 - - - 被收藏 - + 内容警告:%s + 没有描述信息 + 被转嘟 + 被收藏 公开 @@ -435,7 +429,7 @@ 附加留言 转发到 %s 举报失败 - 无法获取状态 + 无法获取嘟文 该报告将发送给给您的服务器管理员。您可以在下面提供有关回报此帐户的原因的说明: 该帐户来自其他服务器。向那里发送一份匿名的报告副本? 账户 @@ -533,4 +527,13 @@ 14 天 365 天 撰写嘟文 + %s 已注册 + 某人进行了注册 + 新用户通知 + 注册 + 登录 + %s 编辑了他们的嘟文 + 我进行过互动的嘟文被编辑了 + 嘟文编辑 + 当你进行过互动的嘟文被编辑时发出通知 \ No newline at end of file From 4c612107a3e0a0248b8cae24909379c1ae38050d Mon Sep 17 00:00:00 2001 From: GunChleoc Date: Sat, 30 Apr 2022 09:17:55 +0000 Subject: [PATCH 051/104] Translated using Weblate (Gaelic) Currently translated at 100.0% (478 of 478 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index cc46053b..70720c2a 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -329,7 +329,7 @@ an ceann %du an ceann %dl an ceann %db - Iarrar leantainn orm + Iarrtas leantainn air Videothan Dealbhan Pròifil Tusky @@ -555,4 +555,5 @@ Deasachadh puist Brathan nuair a thèid postaichean a rinn thu conaltradh leotha a dheasachadh chaidh post a rinn mi conaltradh leis a deasachadh + Clàraich a-steach \ No newline at end of file From 260e25a0a4810fc6dc0dc5747f6aba975aa56d7c Mon Sep 17 00:00:00 2001 From: Martin Marconcini Date: Sat, 30 Apr 2022 19:05:44 +0200 Subject: [PATCH 052/104] Issue 2379: Make it possible to select text in posts. (#2472) * Set TextIsSelectable on the corresponding view holders and remove a longpress listener to let Android select text. * Revert changes, and make selectable text only in detailed status. Remove long press listener to copy to clipboard (as it interferes with natural text selection on Android). * Remove unused string (copy_to_clipboard_success) from all translations. Co-authored-by: Martin Marconcini --- .../tusky/adapter/StatusDetailedViewHolder.java | 16 ---------------- app/src/main/res/layout/item_status_detailed.xml | 1 + 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-ca/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-hi/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-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-pl/strings.xml | 1 - app/src/main/res/values-pt-rBR/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-si/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-ta/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 - app/src/main/res/values/strings.xml | 1 - 47 files changed, 1 insertion(+), 61 deletions(-) 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 1aebf1a7..bf2c05e0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -1,13 +1,10 @@ package com.keylesspalace.tusky.adapter; -import android.content.ClipData; -import android.content.ClipboardManager; import android.content.Context; import android.graphics.drawable.Drawable; import android.text.method.LinkMovementMethod; import android.view.View; import android.widget.TextView; -import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -119,19 +116,6 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { setApplication(status.getActionable().getApplication()); - View.OnLongClickListener longClickListener = view -> { - TextView textView = (TextView) view; - ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("toot", textView.getText()); - clipboard.setPrimaryClip(clip); - - Toast.makeText(view.getContext(), R.string.copy_to_clipboard_success, Toast.LENGTH_SHORT).show(); - - return true; - }; - - content.setOnLongClickListener(longClickListener); - contentWarningDescription.setOnLongClickListener(longClickListener); setStatusVisibility(status.getActionable().getVisibility()); } } diff --git a/app/src/main/res/layout/item_status_detailed.xml b/app/src/main/res/layout/item_status_detailed.xml index 6301641c..28669d23 100644 --- a/app/src/main/res/layout/item_status_detailed.xml +++ b/app/src/main/res/layout/item_status_detailed.xml @@ -123,6 +123,7 @@ android:hyphenationFrequency="full" android:importantForAccessibility="no" android:lineSpacingMultiplier="1.1" + android:textIsSelectable="true" android:textColor="?android:textColorPrimary" android:textSize="?attr/status_text_large" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 07cb8395..cb693670 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -298,7 +298,6 @@ تم الاحتفاظ بنسخة مِن التبويق في مسوداتك حرر لا يحتوي مثيل خادومكم %s على أية حزمة إيموجي مخصصة - تم نسخه إلى الحافظة نوع الإيموجي الإفتراضي في النظام يجب عليك أولا تنزيل حزمة الإيموجي هذه diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index a6ca7a65..092db218 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -123,7 +123,6 @@ Извършва се търсене… По подразбиране от системата Стил на емоджи - Копирано в клипборда Инстанцията ви %s няма персонализирани емоджита Композиране Копие от публикацията е запазено във вашите чернови diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 082f6dea..7d87e3ed 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -48,7 +48,6 @@ আপনাকে প্রথমে এই ইমোজি সেটগুলি ডাউনলোড করতে হবে সিস্টেমের ডিফল্ট ইমোজি স্টাইল - ক্লিপবোর্ডে অনুলিপি করা হয়েছে আপনার ইনস্ট্যান্স %s এর কোনো কাস্টম ইমোজিস নেই রচনা টুট এর একটি কপি আপনার ড্রাফটে সংরক্ষণ করা হয়েছে diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml index 4baa89d0..839e5969 100644 --- a/app/src/main/res/values-bn-rIN/strings.xml +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -304,7 +304,6 @@ টুট এর একটি কপি আপনার ড্রাফটে সংরক্ষণ করা হয়েছে রচনা আপনার ইনস্ট্যান্স %s এর কোনো কাস্টম ইমোজিস নেই - ক্লিপবোর্ডে অনুলিপি করা হয়েছে ইমোজি স্টাইল সিস্টেমের ডিফল্ট আপনাকে প্রথমে এই ইমোজি সেটগুলি ডাউনলোড করতে হবে diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 44f928ff..8b966061 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -304,7 +304,6 @@ Una copia del toot s\'ha guardat a esborranys Escriure La teva instància %s no te emojis personalitzats - Copia al porta papers Estil dels emojis Sistema per defecte Hauràs de descarregar el joc d\'emojis diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index ad91f902..783bbcff 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -375,7 +375,6 @@ تۆ پێویستە سەرەتا ئەم سێتە ئیمۆجییانە دابگریت سیستەمی بنەڕەت شێوازی ئیمۆجی - ڕوونووسکراوە بۆ کلیپ بۆرد نموونەکەت %s هیچ ئیمۆجییەکی ئاسایی نییە دروستکردن کۆپیەکی دەستنووسەکە خەزن کراوە بۆ ڕەشنووسەکانت diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 5f5716c0..b045aafe 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -302,7 +302,6 @@ Kopie vašeho tootu byla uložena do vašich konceptů Napsat Vaše instance %s nemá žádná vlastní emoji - Zkopírováno do schránky Styl emoji Výchozí nastavení systému Musíte si nejprve stáhnout tyto sady emoji diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index 80365d40..78986395 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -251,7 +251,6 @@ Cadwyd copi o\'r tŵt i\'ch drafftiau Creu Nid oes gan eich achos %s emoji bersonol - Copïwyd i\'r clipfwrdd Arddull emoji Rhagosodiad system Bydd angen i chi lawrlwytho\'r setiau emoji hyn yn gyntaf diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b82f4b1d..254a6116 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -279,7 +279,6 @@ Eine Kopie des Beitrags wurde in deine Entwürfe gespeichert Beitrag erstellen Deine Instanz %s hat keine Emojis definiert - In die Zwischenablage kopiert Emoji-Stil System-Standard Du musst diese Emoji-Sets zunächst herunterladen diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 2c03011b..c0cfef4b 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -299,7 +299,6 @@ Kopio de la mesaĝo estis konservita en viaj malnetoj Verki Via nodo %s ne havas proprajn emoĝiojn - Kopiita en tondujo Stilo de emoĝioj Sistema valoro Vi unue devos elŝuti ĉi tiujn emoĝiarojn diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 95f82051..db50c17e 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -269,7 +269,6 @@ Una copia del estado se ha guardado en borradores Redactar Su instancia %s no ofrece emojis personalizados - Copiado al portapapeles Estilo de los emojis Sistema Tendrás que descargarlos primero diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 10caf926..19ac2ebd 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -253,7 +253,6 @@ Tutaren kopia zirriborroetan sartu da Idatzi %s instantziak ez ditu emoji pertsonalizatuak eskaintzen - Arbelean kopiatua Emojien estiloa Sistema Lehenago jaitsi beharko dituzu diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 62da30e2..31ae0f9a 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -248,7 +248,6 @@ رونوشتی از بوق در پیش‌نویس‌هایتان ذخیره شد ایجاد نمونه‌تان %s هیچ اموجی سفارشی‌ای ندارد - در تخته‌گیره رونوشت شد سبک اموجی پیش‌گزیدهٔ سامانه نخست باید این مجموعه‌های اموجی را بارگیری کنید diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 248d4f0d..37f8b984 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -304,7 +304,6 @@ Une copie du pouet a été sauvegardée dans vos brouillons Écrire Votre instance %s n’a pas d’émojis personnalisés - Copié dans le presse-papier Style d’émojis Par défaut du système Vous devez commencer par télécharger ces jeux d’émojis diff --git a/app/src/main/res/values-fy/strings.xml b/app/src/main/res/values-fy/strings.xml index 97ad4052..f902d742 100644 --- a/app/src/main/res/values-fy/strings.xml +++ b/app/src/main/res/values-fy/strings.xml @@ -4,7 +4,6 @@ Dit mei net leech wêze. Systeem standert Emoji styl - Nei it klemboerd kopiearre Gearstelle Ferstjoeren ôfbrutsen Toots oan it ferstjoeren diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index 5cdf52f4..2ea95dc9 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -339,7 +339,6 @@ Sábháladh cóip den tút ar do dhréachtaí Cum Níl aon emojis saincheaptha ag do shampla %s - Cóipeáladh chuig an gearrthaisce Stíl Emoji Réamhshocrú an chórais Beidh ort na tacair emoji seo a íoslódáil ar dtús diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 70720c2a..10d64e0c 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -239,7 +239,6 @@ Feumaidh tu na seataichean seo de dh’Emojis a luchdadh a-nuas an toiseach Bun-roghainn an t-siostaim Stoidhle nan Emojis - Chaidh lethbhreac dheth a chur air an stòr-bhòrd Chan eil Emojis gnàthaichte aig an ionstans %s agad Chaidh lethbhreac dhen phost agad a shàbhaladh ’na dhreachd Chaidh sgur dhen chur diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index cf0a5083..5b488df3 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -247,7 +247,6 @@ Deberás descargar primeiro estos conxuntos de emojis Por defecto no sistema Estilo dos emoji - Copiado ao portapapeis A túa instancia %s non ten emojis personalizados Redactar Gardouse unha copia do toot nos borradores diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index d6b7f8ca..2ecee541 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -306,7 +306,6 @@ बाद में एप्लिकेशन को पुनः आरंभ की आवश्यकता है आपको पहले इस इमोजी सेट को डाउनलोड करना होगा - क्लिपबोर्ड पर कॉपी किया गया लिखें टूट की एक प्रति आपके ड्राफ्ट में सहेज ली गई है टूट भेजने में त्रुटि diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 18a4bb8a..31911d65 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -248,7 +248,6 @@ A bejegyzés másolatát elmentettük a piszkozataid közé Szerkesztés A %s példánynak nincsenek egyedi emoji-jai - Vágólapra másolva Emoji stílus Rendszer alapértelmezés Először le kell töltened ezeket az emoji készleteket diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 46948bf0..14a38b27 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -317,7 +317,6 @@ Afrit af tístinu þínu hefur verið vistað drögunum þínum Semja skilaboð Tilvikið þitt %s er ekki með nein sérsniðin tjáningartákn - Afritað á klippispjald Stíll tjáningartákna Sjálfgefið í kerfinu Þú þarft fyrst að ná í þessi táknmyndasett diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 9a5f3173..b1a3e130 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -297,7 +297,6 @@ Una copia del toot è stata salvata nelle tue bozze Componi La tua istanza %s non ha nessuna emoji personalizzata - Copiato negli appunti Stile di emoji Predefiniti del sistema Dovrai prima scaricare questo pacchetto di emoji diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 1b9a2f16..179b7e8b 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -275,7 +275,6 @@ トゥートのコピーが下書きに保存されました 新規投稿 インスタンス %s にはカスタム絵文字がありません - クリップボードにコピーされました 絵文字スタイル システムのデフォルト 最初にこれらの絵文字セットをダウンロードする必要があります diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 24cfbdad..fc81f2d3 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -311,7 +311,6 @@ 복사본이 임시 저장에 저장되었습니다 글쓰기 이 인스턴스 %s 은(는) 커스텀 이모지가 없습니다. - 클립보드에 복사되었습니다 이모지 스타일 시스템 기본 시스템 기본 외의 이모지를 설정하시려면 우선 다운로드해야 합니다 diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 6f4ebea9..fc3c1705 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -277,7 +277,6 @@ Een kopie van de toot werd opgeslagen als concept Toot schrijven Jouw server %s heeft geen lokale emojis - Naar het klembord gekopieerd Emojistijl Systeemstandaard Je moet eerst deze emoji-sets downloaden diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index 351c0386..2974e98d 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -278,7 +278,6 @@ En kopi av tootet er lagret i kladdene dine Skriv Instansen %s har ingen egendefinerte emojis - Kopiert til utklippstavlen Emoji-stil Systemstandard Du må laste ned emoji-samlingene før de kan brukes diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml index 4dc4370f..b7b6624f 100644 --- a/app/src/main/res/values-oc/strings.xml +++ b/app/src/main/res/values-oc/strings.xml @@ -243,7 +243,6 @@ Una còpia del tut es estat salvat dins los borrolhons Redactar L’instància %s es pas compatibla amb los emoji personalizats - Copiat al quichapapièr Estil dels Emoji Çò del sistèma D’en primièr vos cal telecargar los emojis seguents diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index eb1bf573..a1424b3a 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -244,7 +244,6 @@ Kopia wpisu została zapisana jako szkic Nowy wpis Twoja instancja %s nie używa żadnych niestandardowych emoji - Skopiowano do schowka Styl emoji Domyślny systemu Musisz najpierw pobrać te zestawy emoji diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index e4994c04..0cd82676 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -261,7 +261,6 @@ Uma cópia do toot foi salva nos seus rascunhos Compor A sua instância %s não possui emojis personalizados - Copiado para a área de transferência Estilo de emoji Padrão do sistema É necessário baixar estes pacotes de emojis primeiro diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index b5ca508b..2d49aac5 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -324,7 +324,6 @@ Копия поста сохранена в ваши черновики Сочинить У вашего узла %s нет собственных эмодзи - Скопировано в буфер обмена Стиль эмодзи Системный Сперва эти наборы эмодзи нужно скачать diff --git a/app/src/main/res/values-sa/strings.xml b/app/src/main/res/values-sa/strings.xml index b69336ed..32b651bf 100644 --- a/app/src/main/res/values-sa/strings.xml +++ b/app/src/main/res/values-sa/strings.xml @@ -317,7 +317,6 @@ प्राक्तु भावचिह्नसमूहोऽयमवारोप्यः प्रणाल्यां पूर्वनिविष्टम् भावचिह्नशैली - अंशफलकेऽनुसृतम् भवदीयं विशिष्टस्थलं %s स्वीयानुकूलभावचिह्नरहितं वर्तते लिख्यताम् दौत्यप्रतिलिपिस्तत्र विकर्षेसु रक्षिता diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index df654a29..7ba70498 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -171,7 +171,6 @@ \n https://tusky.app පිළිගන්න පැ. %d කින් - පසුරුපුවරුවට පිටපත් විය මතවිමසුම ඉවත් කරන්න මාධ්‍ය එකතු කරන්න diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 8281dff3..4aa29e3d 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -276,7 +276,6 @@ Kopija tuta je bila shranjena v osnutke Sestavi Vaše vozlišče %s nima emotikonov po meri - Kopirano v odložišče Slog emotikonov Privzete nastavitve sistema Najprej boste morali prenesti te emotikone diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 2dbc0aa5..5ee0f086 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -297,7 +297,6 @@ En kopia av tooten har sparats i dina utkast Skriv Din instans %s har inga anpassade emojis - Kopierat till urklipp Emojis Systemstandard Du behöver ladda ned dessa emojis först diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 3b21fa71..3bedb002 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -231,7 +231,6 @@ நகலெடுக்கபட்ட toot வரைவில் சேமிக்கபட்டது எழுது தங்கள் %s instance(களம்)-ல் எந்தவொரு custom emojis-ம் இல்லை - பிடிப்புப்பலகையில் நகலெடுக்க Emoji பாணி அமைப்பின் இயல்புநிலை தாங்கள் முதலில் இந்த Emoji sets-னை பதிவிறக்கவேண்டும் diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index b45f9658..98e6ab1e 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -116,7 +116,6 @@ ต้องดาวน์โหลดชุดเอโมจิเหล่านี้ก่อน ค่าปริยายของระบบ รูปแบบเอโมจิ - คัดลอกไปยังคลิบบอร์ดแล้ว Instance %s ไม่มีเอโมจิแบบกำหนดเอง เขียน สำเนา Toot บันทึกเป็นฉบับร่างแล้ว diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 42998606..b097977a 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -261,7 +261,6 @@ Tootun bir kopyası taslaklara kaydedildi Oluştur %s örneğinizin herhangi bir özel ifadesi yok - Panoya kopyalandı İfade stili Sistem varsayılanı Önce bu ifade paketini indirmeniz gerekecek diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d9fab3a3..5f101944 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -181,7 +181,6 @@ Спочатку потрібно буде завантажити ці набори емодзі Типовий системний Стиль емодзі - Скопійовано до буфера обміну Ваш сервер %s не має власних емодзі Зберегти чернетку\? Вимагає затвердження підписників власноруч diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 0746ac6b..1ec9ef80 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -430,7 +430,6 @@ Bạn cần tải về bộ emoji này trước Mặc định của thiết bị Emoji - Đã chép vào clipboard Viết Lưu nháp\? Tự bạn sẽ phê duyệt người theo dõi diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6467706a..5dd1975f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -306,7 +306,6 @@ 嘟文副本已保存为草稿 发表嘟文 当前实例 %s 没有自定义表情符号 - 已复制到剪贴板 表情符号风格 系统默认 需要下载表情符号数据 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index f6c9032a..89bd90de 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -304,7 +304,6 @@ 嘟文已儲存為草稿 新嘟文 當前站點 %s 沒有自訂表情符號 - 已複製到剪貼簿 表情符號風格 系統預設 你需要先下載這些表情符號包 diff --git a/app/src/main/res/values-zh-rMO/strings.xml b/app/src/main/res/values-zh-rMO/strings.xml index 878018dd..288af1b3 100644 --- a/app/src/main/res/values-zh-rMO/strings.xml +++ b/app/src/main/res/values-zh-rMO/strings.xml @@ -298,7 +298,6 @@ 嘟文已儲存為草稿 發表新嘟文 當前站點 %s 沒有自訂表情符號 - 已複製到剪貼簿 表情符號風格 系統預設 你需要先下載這些表情符號包 diff --git a/app/src/main/res/values-zh-rSG/strings.xml b/app/src/main/res/values-zh-rSG/strings.xml index aa22205b..bc377597 100644 --- a/app/src/main/res/values-zh-rSG/strings.xml +++ b/app/src/main/res/values-zh-rSG/strings.xml @@ -302,7 +302,6 @@ 嘟文已保存为草稿 新嘟文 当前实例 %s 没有自定义表情符号 - 已复制到剪贴板 表情符号风格 系统默认 需要下载表情符号数据 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 7acfa268..9fd135f4 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -304,7 +304,6 @@ 嘟文已儲存為草稿 發表新嘟文 當前站點 %s 沒有自訂表情符號 - 已複製到剪貼簿 表情符號風格 系統預設 你需要先下載這些表情符號包 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64ab3a1c..2a209d9a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -412,7 +412,6 @@ Compose Your instance %s does not have any custom emojis - Copied to clipboard Emoji style System default You\'ll need to download these emoji sets first From f80994956ada0a2be000974c464331153c19dfec Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Sat, 30 Apr 2022 17:19:26 +0000 Subject: [PATCH 053/104] =?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% (478 of 478 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 2974e98d..3512b480 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -526,4 +526,5 @@ et innlegg jeg har hatt en interaksjon med er redigert Redigerte innlegg Varslinger når et innlegg du har hatt en interaksjon med er redigert + Innlogging \ No newline at end of file From 40a6d6a08a7d879315a544d20596c1446bc534f2 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Sat, 30 Apr 2022 17:19:26 +0000 Subject: [PATCH 054/104] Translated using Weblate (Ukrainian) Currently translated at 100.0% (478 of 478 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 | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 5f101944..7d4b806e 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -548,4 +548,5 @@ допис, з яким у мене була взаємодія, відредаговано Сповіщення, коли редагується повідомлення, з яким ви взаємодіяли Редакції допису + Вхід \ No newline at end of file From ba8747a99af9f126e7f53670fd07160a33ca723f Mon Sep 17 00:00:00 2001 From: Roj Date: Sat, 30 Apr 2022 17:19:26 +0000 Subject: [PATCH 055/104] Translated using Weblate (Sorani) Currently translated at 88.4% (423 of 478 strings) Co-authored-by: Roj Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ckb/ Translation: Tusky/Tusky --- app/src/main/res/values-ckb/strings.xml | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index 783bbcff..ba275b0b 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -23,7 +23,7 @@ هاشتاگی پیشاندانی دڵخوازەکان پیشاندانی بەهێزکردنەکان - کردنەوەی بەهێزکردنی نووسەر + پۆستکەرەوەکە ببینە هاشتاگ ئاماژەکان بەستەرەکان @@ -57,14 +57,14 @@ وێنە بگرە زیادکردنی ڕاپرسی زیادکردنی میدیا - کردنەوە لە وێبگەڕ + لە وێبگەڕ بیکەوە میدیا بەدواداچونی داواکاریەکان بکە دۆمەینە شاراوەکان بەکارهێنەرە بلۆککراوەکان بەکارهێنەرە گۆڕاوەکان نیشانەکان - دڵخوازەکان + بەدڵبوونەکان پەسەندکراوەکانی ئەژمێر پەسەندەکان پرۆفایل @@ -76,12 +76,12 @@ سڕینەوە دەستکاری گوزارشەکان - پیشاندانی بەهێزکردنەکان + پۆستکردنەوەکان نیشان بدە شاردنەوەی بەهێزکردنەکان بەربەست کردن لاببە بلۆک بەدوادانەچو - بەدواداکەوتن + شوێنی بکەوە ئایا دڵنیایت لەوەی دەتەوێت بچیتەدەرەوە لە هەژماری %1$s؟ چوونەدەرەوە چوونەژوورەوە لەگەڵ ماستۆدۆن @@ -90,7 +90,7 @@ لابردنی دڵخوازەکان نیشانه دڵخواز - لابردنی بەهێزکردن + پۆستکردنەوەکە بگەڕێنەوە بەهێزکردن وەڵام وەڵامدانەوەی خێرا @@ -110,7 +110,7 @@ کرتە بکە بۆ بینین میدیا شاراوە ناوەڕۆکی هەستیار - %s بەرزکرا + %s پۆستی کردەوە \@%s مۆڵەتەکان ڕاگه یه نراوەکان @@ -122,11 +122,11 @@ بەکارهێنەرە بێدەنگ نیشانەکان دڵخوازەکان - شوێنکەوتوان + شوێنکەوتوو شوێنکەوتنەکان چەسپا لەگەڵ وەڵامەکان - بابەتەکان + پۆست زنجیرە سەرخشتەکان نامە ڕاستەوخۆکان @@ -249,7 +249,7 @@ \n \nکارتێکردنی ئاگانامەکانی پاڵپێوەنان، بەڵام دەتوانیت بە پەسەندکردنە ئاگانامەکانت دا بخشێنیەوە بە دەستی. ڕزگارکرا - تێبینی تایبەتی تۆ دەربارەی ئەم ئەژمێرە + تێبینیی تایبەتیت بۆ ئەم هەژمارە Wellbeing شاردنەوەی ناونیشانی شریتی ئامڕازی سەرەوە پیشاندانی دیالۆگی دووپاتکردنەوە پێش بەهێزکردن @@ -294,7 +294,7 @@ ڕاپرسییەک کە دروستت کردووە کۆتایی هات ڕاپرسییەک کە دەنگی پێداویت کۆتایی هات دەنگ - داخراوە + کۆتایی هاتووە کۆتایی دێت لە %s %s کەس @@ -336,10 +336,10 @@ %1$s و %2$s %1$s پەسەندکراوە لەلایەن - بەرزکراوە لەلایەن + پۆست کراوەتەوە لەلایەن - %s بەهێزکردن - %s بەهێزکردن + %s پۆستکردنەوە + %s پۆستکردنەوە %1$s دڵخواز From 8fa5141f27e063ad0ab1795eefc738beed54967d Mon Sep 17 00:00:00 2001 From: Connyduck Date: Sat, 30 Apr 2022 17:19:26 +0000 Subject: [PATCH 056/104] Added translation using Weblate (Portuguese (Portugal)) Co-authored-by: Connyduck --- app/src/main/res/values-pt-rPT/strings.xml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 app/src/main/res/values-pt-rPT/strings.xml diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml new file mode 100644 index 00000000..a6b3daec --- /dev/null +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file From 2ccc87231da9a05edcb34f8ef6d765e7af8f95bc Mon Sep 17 00:00:00 2001 From: Bruno Miguel Date: Sat, 30 Apr 2022 17:19:26 +0000 Subject: [PATCH 057/104] Translated using Weblate (Portuguese (Portugal)) Currently translated at 100.0% (477 of 477 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 | 531 ++++++++++++++++++++- 1 file changed, 530 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index a6b3daec..e501ed2a 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -1,2 +1,531 @@ - \ No newline at end of file + + Maior + Toots novos + Criação de contas + %1$s e %2$s + + %d nova interação + %d novas interações + + A responder a @%s + Editar a lista + Exige a aprovação manual de seguidores + Guardar rascunho\? + Depois + Desafixar + %1$s e %2$s + Bem-estar + Escrever Toot + Deseja excluir a lista %s\? + Apesar do seu perfil não ser trancado, %1$s exige que você revise a solicitação para te seguir destes perfis manualmente. + Notificar + Cancelar + Autorização negada. + Erro ao adquirir token de login. + O toot é muito longo! + 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. + É necessária permissão para escrever no armazenamento. + Não é possível anexar imagens e vídeos no mesmo toot. + Erro ao enviar. + Erro ao publicar o toot. + Página inicial + Notificações + Local + Federada + Mensagens Diretas + Separadores + Conversa + Toots + Com respostas + Fixado + Segue + Seguidores + Favoritos + Itens guardados + Utilizadors silenciados + Utilizadors bloqueados + Instâncias bloqueadas + Seguidores Pendentes + Conteúdo sensível + Editar perfil + Conteúdo ocultado + Rascunhos + Toque para ver + Mostrar Mais + Mostrar Menos + Expandir + Contrair + Toots agendados + Anúncios + Licenças + \@%s + %s fez boost + Nada aqui. + Nada aqui. Arraste para baixo para atualizar! + %s fez boost ao seu toot + %s adicionou o seu toot aos favoritos + %s está a seguir-te + %s pediu para te seguir + %s criou conta + %s acabou de publicar um toot + %s editou um toot + Denunciar @%s + Comentários adicionais\? + Resposta rápida + Responder + Fazer boost + Desfazer boost + Adicionar aos favoritos + Remover dos favoritos + Guardar + Remover dos itens guardados + Mais + Escrever + Entrar com Mastodon + Sair + Tem certeza de que deseja sair da conta %1$s\? + Seguir + Deixar de seguir + Bloquear + Desbloquear + Esconder boosts + Mostrar boosts + Denunciar + Editar + Apagar + Apagar conversa + Apagar e criar novo rascunho + TOOT + TOOT! + Tentar novamente + Fechar + Perfil + Preferências + Preferências da Conta + Favoritos + Guardados + utilizadors silenciados + utilizadors bloqueados + Instâncias bloqueadas + Seguidores Pendentes + Conteúdo multimédia + Abrir no navegador + Adicionar conteúdo multimédia + Adicionar votação + Tirar foto + Partilhar + Silenciar + Remover silêncio + Remover %s do silêncio + Remover notificações de %s do silêncio + Silencie notificações de %s + Silenciar %s + Remover %s do silêncio + Silenciar conversa + Remover conversa do silêncio + Mencionar + Esconder conteúdo multimédia + Abrir menu + Pesquisar + Rascunhos + Toots agendados + Privacidade do toot + Aviso de conteúdo + Teclado de emojis + Agendar toot + Redefinir + Adicionar Separador + Hiperligações + Menções + Hashtags + Ver quem fez boost + Mostrar boosts + Mostrar favoritos + Hashtags + Menções + Hiperligações + Abrir conteúdo multimédia #%d + A descarregar %1$s + Copiar hiperligação + Abrir como %s + Partilhar como… + Descarregar conteúdo multimédia + A descarregar conteúdo multimédia + Partilhar hiperligação do toot via… + Partilhar toot via… + Partilhar conteúdo multimédia via… + Enviado! + Utilizador desbloqueado + Utilizador removido do silêncio + %s desbloqueada + Enviado! + Resposta enviada com sucesso. + Que instância\? + Em que está a pensar\? + Aviso de conteúdo + Nome + Biografia + Pesquisar… + Sem resultados + Responder… + Avatar + Cabeçalho + O que é uma instância\? + A ligar… + O endereço IP ou domínio de qualquer instância pode ser inserido aqui, como por exemplo mastodon.social, masto.donte.com.br, colorid.es ou qualquer outro! +\n +\n Se ainda não tem uma conta, insira o nome da instância onde pretende participar e crie uma conta lá. +\n +\n Uma instância é um lugar onde sua conta é hospedada, mas pode facilmente seguir e comunicar com pessoas de outras instâncias como se todos estivessem no mesmo site. +\n +\nMais informações disponíveis em joinmastodon.org. + Envio de Conteúdo Multimédia Terminando + A enviar… + Descarregar + Cancelar pedido para seguir\? + Deixar de seguir esta conta\? + Apagar este toot\? + Apagar e criar novo rascunho\? + Apagar esta conversa\? + Tem certeza que pretende bloquear a instância %s\? Deixará de poder ver quaisquer conteúdos dessa instância em qualquer timeline pública ou nas suas notificações. Os seus seguidores dessa instância serão removidos. + Bloquear instância + Bloquear @%s\? + Silenciar @%s\? + Esconder notificações + Público: Publicar em timelines públicas + Não listado: Não publicar em timelines públicas + Privado: Publicar apenas para os seguidores + Direto: Publicar apenas para os utilizadores mencionados + Editar notificações + Notificações + Alertas + Notificar com som + Notificar com vibração + Notificar com luz + Notifique-me quando + for mencionado + for seguido + alguém para quem ativei os alertas publicar um toot novo + fizerem pedido para me seguir + derem boosts nos meus toots + adicionarem os meus toots aos favoritos + votações terminarem + alguém criar conta + um toot com o qual interagi for editado + Aparência + Temas + Timelines + Filtros + Noturno + Diurno + AMOLED + Automático ao pôr-do-sol + Usar o Tema do Sistema + Navegador + Usar Separadores Personalizados do Chrome + Esconder o botão de criação de toots ao fazer scroll + Idioma + Mostrar indicador para bots + Reproduzir avatares em GIFs + Mostrar desfocagem em conteúdo multimédia sensível + Animar emojis personalizados + Filtro da timeline + Separadors + Mostrar boosts + Mostrar respostas + Mostrar pré-visualização de conteúdo multimédia + Proxy + Proxy HTTP + Ativar proxy HTTP + Servidor da proxy HTTP + Privacidade padrão dos toots + Classificar sempre conteúdo multimédia como sensível + Toots (sincronizados com a instância) + Erro ao sincronizar configurações + Posição do menu principal + Superior + Inferior + Público + Porta da proxy HTTP + Não listado + Privado + Menor + Pequeno + Médio + Grande + Menções Novas + Notificações para menções novas + Novos Seguidores + Tamanho do texto do toot + Notificações para seguidores novos + Seguidores Pendentes + Notificações para seguidores pendentes + Boosts + Votações + Notificações para votações que terminaram + Notificações quando alguém para quem ativei os alertas publicar um toot novo + Notificações para novos utilizadores + Edições a toots + Notificações para boosts recebidos + Favoritos + Notificações quando os teus toots são adicionados aos favoritos + Notificações quando toots com os quais interagi foram editados + %s mencionou-te + %1$s, %2$s, %3$s e %4$d outros + %1$s, %2$s e %3$s + Perfil Bloqueado + Sobre + Tusky %s + A correr o Tusky + Atualizar + Tusky é um software livre e de código aberto e é ljcenciado com a versão 3 da GNU General Public License. Leia a licença aqui: https://www.gnu.org/licenses/gpl-3.0.pt-br.html + Página do projeto: +\n https://tusky.app + Reporte de erros e pedidos de funcionalidades: +\n https://github.com/tuskyapp/Tusky/issues + Perfil do Tusky + Partilhar conteúdo do toot + Partilhar hiperligação do toot + Imagens + Vídeo + Áudio + Anexos + Pedido enviado + em %dy + em %dd + em %dh + em %dm + em %ds + %da + %dd + %dh + %dm + %ds + Segue-te + Mostrar sempre conteúdo multimédia sensível + Expandir sempre toots com Aviso de Conteúdo + Palavra completa + Conteúdo Multimédia + carregar mais + Timelines públicas + Conversas + Criar filtro + Editar filtro + Remover + Se a palavra ou frase for alfanumérica, só será aplicado se corresponder à palavra completa + Frase para filtrar + Adicionar Conta + Adicionar nova Conta Mastodon + Listas + Não foi possível renomear a lista + Listas + Lista da timeline + Não foi possível criar a lista + Não foi possível apagar a lista + Criar uma lista + Renomear a lista + Apagar a lista + Pesquisar pessoas que você segue + Adicionar conta à lista + Remover conta da lista + Publicar com a conta %1$s + Erro ao incluir descrição + + Descrição para deficientes visuais +\n(até %d caracteres) + + + Descrever + Remover + Bloquear perfil + A enviar o toot… + Erro ao enviar o toot + A Enviar os Toots + Envio cancelado + Uma cópia do toot foi guardada nos seus rascunhos + Escrever + A sua instância, %s, não tem emojis personalizados + Estilo de emoji + Padrão do sistema + É necessário descarregar estes pacotes de emojis primeiro + A fazer pesquisa… + Expandir/Contrair todos os toots + Abrir toot + É necessário reiniciar a aplicação + É necessário reiniciar o aplicativo para aplicar as alterações + Reiniciar + Pacote de emojis padrão do seu dispositivo + Emojis padrão do Android da versão 4.4 até 7.1 + Pacote de emojis padrão do Mastodon + Pacote de emojis atual do Google + Erro ao baixar + Robô + %1$s mudou-se para: + Dar boost para o mesmo público + Desfazer boost + O Tusky contém código e recursos dos seguintes projetos de código aberto: + Licenciado sob a licença Apache (cópia separadorixo) + CC-BY 4.0 + CC-BY-SA 4.0 + Metadados do perfil + Adicionar + Rótulo + Conteúdo + Usar tempo absoluto + As informações separadorixo podem refletir incompletamente o perfil do utilizador. Toque aqui para abrir o perfil completo no navegador. + Fixar + + %1$s Favorito + %1$s Favoritos + + + %s Boost + %s Boosts + + Levou boost de + Favoritado por + %1$s + %1$s, %2$s e %3$d outros + + excedeu o máximo de %1$d separador + excedeu o máximo de %1$d separadors + + conteúdo multimédia: %s + Aviso de Conteúdo: %s + Sem descrição + Você fez boost + Favoritado + Salvo + Público + Não-listado + Privado + Direto + Enquete com as opções: %1$s, %2$s, %3$s, %4$s; %5$s + Nome da lista + Adicionar hashtag + Hashtag sem # + Hashtags + Selecionar lista + Lista + Limpar + Filtro + Salvar + Compor toot + Compor + Tem certeza de que deseja limpar permanentemente todas as suas notificações\? + Opções para imagem %s + %1$s • %2$s + + %s voto + %s votos + + + %s pessoa + %s pessoas + + termina em %s + Terminou + Votar + Uma enquete que você votou terminou + Sua enquete terminou + + %d dia restante + %d dias restantes + + + %d hora restante + %d horas restantes + + + %d minuto restante + %d minutos restantes + + + %d segundo restante + %d segundos restantes + + Continuar + Voltar + Ok + \@%s denunciado com sucesso + Comentários adicionais + Encaminhar para %s + Erro ao denunciar + Erro ao carregar toots + A denúncia será enviada aos moderadores da instância. Explique por que denunciou a conta: + A conta está em outra instância. Enviar uma cópia anônima da denúncia para lá\? + Contas + Erro ao pesquisar + Mostrar filtro de notificações + Ativar deslizar para alternar entre separadors + Enquete + Duração + Indefinido + 5 minutos + 30 minutos + 1 hora + 6 horas + 1 dia + 3 dias + 7 dias + 14 dias + 30 dias + 60 dias + 90 dias + 180 dias + 365 dias + Adicionar opção + Múltiplas opções + Opção %d + Editar + Erro ao pesquisar %s + Sem rascunhos. + Sem toots agendados. + Salvo! + Algumas informações que podem afetar seu bem-estar serão ocultadas. Isso inclui: +\n +\n- Notificações de favoritos, boosts e seguidores +\n- Número de favoritos e boosts nos toots +\n- Status de toots e seguidores nos perfis +\n +\nNotificações push não serão afetadas, mas é possível revisar sua preferência manualmente. + Revisar notificações + Limitar notificações da timeline + Sem comunicados. + Mastodon possui um intervalo mínimo de 5 minutos para agendar. + Mostrar prévias de Hiperligações nas linhas + Solicitar confirmação antes de dar boost + Solicitar confirmação antes de favoritar + Esconder o título da barra superior de tarefas + Nota pessoal sobre este perfil aqui + Esconder status dos toots + Esconder status dos perfis + + Não é possível anexar mais de %1$d arquivo de conteúdo multimédia. + Não é possível anexar mais de %1$d arquivos de conteúdo multimédia. + + Erro ao enviar o toot! + Erro ao carregar toot para responder + Rascunho excluído + O toot em que se rascunhou uma resposta foi excluído + Ocorreu um erro. + Ocorreu um erro de conetividade! Por favor, verifique a sua ligação e tente novamente! + Isto não pode estar vazio. + Instância inválida inserida + Erro ao autenticar com esta instância. + Nao foi possível encontrar um navegador. + Ocorreu um erro não identificado de autorização. + Entrar + Guardar + Editar perfil + Editar + Desfazer + Aceitar + Rejeitar + \ No newline at end of file From ce5ec15ff13ba0b36b486edd192dbb6c103a2f6c Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 1 May 2022 17:16:22 +0200 Subject: [PATCH 058/104] increase timeout for media uploads (#2489) --- .../tusky/components/compose/MediaUploader.kt | 6 +++--- .../keylesspalace/tusky/di/NetworkModule.kt | 15 +++++++++++++++ .../tusky/network/MastodonApi.kt | 7 ------- .../tusky/network/MediaUploadApi.kt | 19 +++++++++++++++++++ 4 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt 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 85be146c..f1debc98 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 @@ -26,7 +26,7 @@ import androidx.core.net.toUri import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia -import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.network.MediaUploadApi import com.keylesspalace.tusky.network.ProgressRequestBody import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN import com.keylesspalace.tusky.util.getImageSquarePixels @@ -75,7 +75,7 @@ class CouldNotOpenFileException : Exception() class MediaUploader @Inject constructor( private val context: Context, - private val mastodonApi: MastodonApi + private val mediaUploadApi: MediaUploadApi ) { @OptIn(ExperimentalCoroutinesApi::class) @@ -222,7 +222,7 @@ class MediaUploader @Inject constructor( null } - val result = mastodonApi.uploadMedia(body, description).getOrThrow() + val result = mediaUploadApi.uploadMedia(body, description).getOrThrow() send(UploadEvent.FinishedEvent(result.id)) awaitClose() } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index d8b52ca3..90dd3026 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -24,6 +24,7 @@ import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.network.MediaUploadApi import com.keylesspalace.tusky.util.getNonNullString import dagger.Module import dagger.Provides @@ -112,4 +113,18 @@ class NetworkModule { @Provides @Singleton fun providesApi(retrofit: Retrofit): MastodonApi = retrofit.create() + + @Provides + @Singleton + fun providesMediaUploadApi(retrofit: Retrofit, okHttpClient: OkHttpClient): MediaUploadApi { + val longTimeOutOkHttpClient = okHttpClient.newBuilder() + .readTimeout(100, TimeUnit.SECONDS) + .writeTimeout(100, TimeUnit.SECONDS) + .build() + + return retrofit.newBuilder() + .client(longTimeOutOkHttpClient) + .build() + .create() + } } 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 25fe03d4..7357293b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -142,13 +142,6 @@ interface MastodonApi { @POST("api/v1/notifications/clear") fun clearNotifications(): Single - @Multipart - @POST("api/v2/media") - suspend fun uploadMedia( - @Part file: MultipartBody.Part, - @Part description: MultipartBody.Part? = null - ): Result - @FormUrlEncoded @PUT("api/v1/media/{mediaId}") suspend fun updateMedia( diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt new file mode 100644 index 00000000..c7e9633f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt @@ -0,0 +1,19 @@ +package com.keylesspalace.tusky.network + +import com.keylesspalace.tusky.entity.MediaUploadResult +import okhttp3.MultipartBody +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part + +/** endpoints defined in this interface will be called with a higher timeout than usual + * which is necessary for media uploads to succeed on some servers + */ +interface MediaUploadApi { + @Multipart + @POST("api/v2/media") + suspend fun uploadMedia( + @Part file: MultipartBody.Part, + @Part description: MultipartBody.Part? = null + ): Result +} From 444e7365c972d79f54553b841d4a212a52f44a4e Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 3 May 2022 19:12:35 +0200 Subject: [PATCH 059/104] fix race condition where multiple uploaded media can get same internal id (#2479) * fix race condition where multiple uploaded media can get same internal id * atomically update media stateflow * atomically update media stateflow --- .../components/compose/ComposeActivity.kt | 24 +++-- .../components/compose/ComposeViewModel.kt | 99 +++++++++++-------- 2 files changed, 72 insertions(+), 51 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 48e14def..32162614 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 @@ -51,6 +51,7 @@ import androidx.core.view.ContentInfoCompat import androidx.core.view.OnReceiveContentListener import androidx.core.view.isGone import androidx.core.view.isVisible +import androidx.lifecycle.asLiveData import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager @@ -344,14 +345,17 @@ class ComposeActivity : viewModel.statusVisibility.observe { visibility -> setStatusVisibility(visibility) } - viewModel.media.observe { 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) + 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) + } } } + viewModel.poll.observe { poll -> binding.pollPreview.visible(poll != null) poll?.let(binding.pollPreview::setPoll) @@ -364,7 +368,7 @@ class ComposeActivity : } updateScheduleButton() } - combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll -> + combineOptionalLiveData(viewModel.media.asLiveData(), viewModel.poll) { media, poll -> val active = poll == null && media!!.size != 4 && (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE) @@ -781,11 +785,11 @@ class ComposeActivity : spoilerText = binding.composeContentWarningField.text.toString() } val characterCount = calculateTextLength() - if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value!!.isEmpty()) { + if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value.isEmpty()) { binding.composeEditField.error = getString(R.string.error_empty) enableButtons(true) } else if (characterCount <= maximumTootCharacters) { - if (viewModel.media.value!!.isNotEmpty()) { + if (viewModel.media.value.isNotEmpty()) { finishingUploadDialog = ProgressDialog.show( this, getString(R.string.dialog_title_finishing_media_upload), getString(R.string.dialog_message_uploading_media), true, true @@ -983,7 +987,7 @@ class ComposeActivity : } data class QueuedMedia( - val localId: Long, + val localId: Int, val uri: Uri, val type: Type, val mediaSize: Long, 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 81500ee4..7b180532 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -21,6 +21,7 @@ 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 com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia import com.keylesspalace.tusky.components.drafts.DraftHelper @@ -36,15 +37,17 @@ 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.filter -import com.keylesspalace.tusky.util.map import com.keylesspalace.tusky.util.randomAlphanumericString import com.keylesspalace.tusky.util.toLiveData -import com.keylesspalace.tusky.util.withoutFirstWhich import io.reactivex.rxjava3.core.Observable import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.rxSingle import kotlinx.coroutines.withContext @@ -84,10 +87,10 @@ class ComposeViewModel @Inject constructor( val poll: MutableLiveData = mutableLiveData(null) val scheduledAt: MutableLiveData = mutableLiveData(null) - val media = mutableLiveData>(listOf()) + val media: MutableStateFlow> = MutableStateFlow(emptyList()) val uploadError = MutableLiveData() - private val mediaToJob = mutableMapOf() + private val mediaToJob = mutableMapOf() private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty() @@ -103,7 +106,7 @@ class ComposeViewModel @Inject constructor( suspend fun pickMedia(mediaUri: Uri, description: String? = null): Result = withContext(Dispatchers.IO) { try { val (type, uri, size) = mediaUploader.prepareMedia(mediaUri) - val mediaItems = media.value!! + val mediaItems = media.value if (type != QueuedMedia.Type.IMAGE && mediaItems.isNotEmpty() && mediaItems[0].type == QueuedMedia.Type.IMAGE @@ -118,29 +121,31 @@ class ComposeViewModel @Inject constructor( } } - private fun addMediaToQueue( + private suspend fun addMediaToQueue( type: QueuedMedia.Type, uri: Uri, mediaSize: Long, description: String? = null ): QueuedMedia { - val mediaItem = QueuedMedia( - localId = System.currentTimeMillis(), - uri = uri, - type = type, - mediaSize = mediaSize, - description = description - ) - media.postValue(media.value!! + mediaItem) + val mediaItem = media.updateAndGet { mediaValue -> + val mediaItem = QueuedMedia( + localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1, + uri = uri, + type = type, + mediaSize = mediaSize, + description = description + ) + mediaValue + mediaItem + }.last() mediaToJob[mediaItem.localId] = viewModelScope.launch { mediaUploader .uploadMedia(mediaItem) .catch { error -> - media.postValue(media.value?.filter { it.localId != mediaItem.localId } ?: emptyList()) + media.update { mediaValue -> mediaValue.filter { it.localId != mediaItem.localId } } uploadError.postValue(error) } .collect { event -> - val item = media.value?.find { it.localId == mediaItem.localId } + val item = media.value.find { it.localId == mediaItem.localId } ?: return@collect val newMediaItem = when (event) { is UploadEvent.ProgressEvent -> @@ -148,16 +153,14 @@ class ComposeViewModel @Inject constructor( is UploadEvent.FinishedEvent -> item.copy(id = event.mediaId, uploadPercent = -1) } - synchronized(media) { - val mediaValue = media.value!! - val index = mediaValue.indexOfFirst { it.localId == newMediaItem.localId } - media.postValue( - if (index == -1) { - mediaValue + newMediaItem + media.update { mediaValue -> + mediaValue.map { mediaItem -> + if (mediaItem.localId == newMediaItem.localId) { + newMediaItem } else { - mediaValue.toMutableList().also { it[index] = newMediaItem } + mediaItem } - ) + } } } } @@ -165,13 +168,23 @@ class ComposeViewModel @Inject constructor( } private fun addUploadedMedia(id: String, type: QueuedMedia.Type, uri: Uri, description: String?) { - val mediaItem = QueuedMedia(System.currentTimeMillis(), uri, type, 0, -1, id, description) - media.value = media.value!! + mediaItem + media.update { mediaValue -> + val mediaItem = QueuedMedia( + localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1, + uri = uri, + type = type, + mediaSize = 0, + uploadPercent = -1, + id = id, + description = description + ) + mediaValue + mediaItem + } } fun removeMediaFromQueue(item: QueuedMedia) { mediaToJob[item.localId]?.cancel() - media.value = media.value!!.withoutFirstWhich { it.localId == item.localId } + media.update { mediaValue -> mediaValue.filter { it.localId == item.localId } } } fun toggleMarkSensitive() { @@ -211,7 +224,7 @@ class ComposeViewModel @Inject constructor( viewModelScope.launch { val mediaUris: MutableList = mutableListOf() val mediaDescriptions: MutableList = mutableListOf() - media.value?.forEach { item -> + media.value.forEach { item -> mediaUris.add(item.uri.toString()) mediaDescriptions.add(item.description) } @@ -248,14 +261,14 @@ class ComposeViewModel @Inject constructor( Observable.just(Unit) }.toLiveData() - val sendObservable = media + val sendFlow = media .filter { items -> items.all { it.uploadPercent == -1 } } .map { val mediaIds: MutableList = mutableListOf() val mediaUris: MutableList = mutableListOf() val mediaDescriptions: MutableList = mutableListOf() val mediaProcessed: MutableList = mutableListOf() - for (item in media.value!!) { + for (item in media.value) { mediaIds.add(item.id!!) mediaUris.add(item.uri) mediaDescriptions.add(item.description ?: "") @@ -285,17 +298,21 @@ class ComposeViewModel @Inject constructor( serviceClient.sendToot(tootToSend) } - return combineLiveData(deletionObservable, sendObservable) { _, _ -> } + return combineLiveData(deletionObservable, sendFlow.asLiveData()) { _, _ -> } } - suspend fun updateDescription(localId: Long, description: String): Boolean { - val newList = media.value!!.toMutableList() - val index = newList.indexOfFirst { it.localId == localId } - if (index != -1) { - newList[index] = newList[index].copy(description = description) + suspend fun updateDescription(localId: Int, description: String): Boolean { + val newMediaList = media.updateAndGet { mediaValue -> + mediaValue.map { mediaItem -> + if (mediaItem.localId == localId) { + mediaItem.copy(description = description) + } else { + mediaItem + } + } } - media.value = newList - val updatedItem = newList.find { it.localId == localId } + + val updatedItem = newMediaList.find { it.localId == localId } if (updatedItem?.id != null) { return api.updateMedia(updatedItem.id, description) .fold({ @@ -387,8 +404,8 @@ class ComposeViewModel @Inject constructor( val draftAttachments = composeOptions?.draftAttachments if (draftAttachments != null) { // when coming from DraftActivity - draftAttachments.forEach { attachment -> - viewModelScope.launch { + viewModelScope.launch { + draftAttachments.forEach { attachment -> pickMedia(attachment.uri, attachment.description) } } From 1eed0e1cc2fc8c1f5dae8c7e75fc00de118e2b7c Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 3 May 2022 19:13:13 +0200 Subject: [PATCH 060/104] fix unparsed html when sharing status content (#2491) --- .../main/java/com/keylesspalace/tusky/fragment/SFragment.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 bb02807d..ad81abe3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -58,6 +58,7 @@ import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.network.MastodonApi; import com.keylesspalace.tusky.network.TimelineCases; import com.keylesspalace.tusky.util.LinkHelper; +import com.keylesspalace.tusky.util.StatusParsingHelper; import com.keylesspalace.tusky.view.MuteAccountDialog; import com.keylesspalace.tusky.viewdata.AttachmentViewData; @@ -228,7 +229,7 @@ public abstract class SFragment extends Fragment implements Injectable { String stringToShare = statusToShare.getAccount().getUsername() + " - " + - statusToShare.getContent().toString(); + StatusParsingHelper.parseAsMastodonHtml(statusToShare.getContent()).toString(); sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare); sendIntent.putExtra(Intent.EXTRA_SUBJECT, statusUrl); sendIntent.setType("text/plain"); From f3d79238034586d2fb81cff21d8ecdeb3a15ad54 Mon Sep 17 00:00:00 2001 From: Ivan Kupalov Date: Tue, 3 May 2022 19:14:55 +0200 Subject: [PATCH 061/104] Improve UX when Login WebView fails to load the page (#2492) Previously we simply closed the screen with the login WebView which could cause confusion. Now we specify that page could not be loaded. As a side effect it will also show the error message which the server returns (if any). --- .../com/keylesspalace/tusky/components/login/LoginActivity.kt | 4 +++- .../tusky/components/login/LoginWebViewActivity.kt | 2 +- app/src/main/res/values/strings.xml | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt index 4df7abc1..bcbb4abf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt @@ -68,7 +68,9 @@ class LoginActivity : BaseActivity(), Injectable { // Authorization failed. Put the error response where the user can read it and they // can try again. setLoading(false) - binding.domainTextInputLayout.error = getString(R.string.error_authorization_denied) + // Use error returned by the server or fall back to the generic message + binding.domainTextInputLayout.error = + result.errorMessage.ifBlank { getString(R.string.error_authorization_denied) } Log.e( TAG, "%s %s".format( diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt index 58f745e7..a32a164c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt @@ -117,7 +117,7 @@ class LoginWebViewActivity : BaseActivity(), Injectable { error: WebResourceError ) { Log.d("LoginWeb", "Failed to load ${data.url}: $error") - finishWithoutSlideOutAnimation() + sendResult(LoginResult.Err(getString(R.string.error_could_not_load_login_page))) } override fun shouldOverrideUrlLoading( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2a209d9a..fc1ed743 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -9,6 +9,7 @@ An unidentified authorization error occurred. Authorization was denied. Failed getting a login token. + 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. From a2cc622683073862f8842ceb9ed764d32ccf2a47 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 3 May 2022 19:15:59 +0200 Subject: [PATCH 062/104] fix EmojiCompat.get().process crash in polls (#2494) --- .../com/keylesspalace/tusky/adapter/PollAdapter.kt | 10 +++------- .../tusky/components/account/AccountActivity.kt | 10 +--------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt index 9ffeca9e..ef366795 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt @@ -19,7 +19,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.content.ContextCompat -import androidx.emoji2.text.EmojiCompat import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.ItemPollBinding @@ -87,9 +86,8 @@ class PollAdapter : RecyclerView.Adapter>() { when (mode) { RESULT -> { val percent = calculatePercent(option.votesCount, votersCount, voteCount) - val emojifiedPollOptionText = buildDescription(option.title, percent, option.voted, resultTextView.context) + resultTextView.text = buildDescription(option.title, percent, option.voted, resultTextView.context) .emojify(emojis, resultTextView, animateEmojis) - resultTextView.text = EmojiCompat.get().process(emojifiedPollOptionText) val level = percent * 100 val optionColor = if (option.voted) { @@ -103,8 +101,7 @@ class PollAdapter : RecyclerView.Adapter>() { resultTextView.setOnClickListener(resultClickListener) } SINGLE -> { - val emojifiedPollOptionText = option.title.emojify(emojis, radioButton, animateEmojis) - radioButton.text = EmojiCompat.get().process(emojifiedPollOptionText) + radioButton.text = option.title.emojify(emojis, radioButton, animateEmojis) radioButton.isChecked = option.selected radioButton.setOnClickListener { pollOptions.forEachIndexed { index, pollOption -> @@ -114,8 +111,7 @@ class PollAdapter : RecyclerView.Adapter>() { } } MULTIPLE -> { - val emojifiedPollOptionText = option.title.emojify(emojis, checkBox, animateEmojis) - checkBox.text = EmojiCompat.get().process(emojifiedPollOptionText) + checkBox.text = option.title.emojify(emojis, checkBox, animateEmojis) checkBox.isChecked = option.selected checkBox.setOnCheckedChangeListener { _, isChecked -> pollOptions[holder.bindingAdapterPosition].selected = isChecked 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 cc8719bb..6a8bc7dc 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 @@ -37,7 +37,6 @@ import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat.Type.systemBars import androidx.core.view.updatePadding -import androidx.emoji2.text.EmojiCompat import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.viewpager2.widget.MarginPageTransformer @@ -459,14 +458,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI */ private fun updateToolbar() { loadedAccount?.let { account -> - - val emojifiedName = account.name.emojify(account.emojis, binding.accountToolbar, animateEmojis) - - try { - supportActionBar?.title = EmojiCompat.get().process(emojifiedName) - } catch (e: IllegalStateException) { - supportActionBar?.title = emojifiedName - } + supportActionBar?.title = account.name.emojify(account.emojis, binding.accountToolbar, animateEmojis) supportActionBar?.subtitle = String.format(getString(R.string.post_username_format), account.username) } } From f693c5ea210d0e3b774ccc3627a6668cda9e4029 Mon Sep 17 00:00:00 2001 From: Luna Date: Sun, 1 May 2022 17:40:32 +0000 Subject: [PATCH 063/104] Translated using Weblate (Polish) Currently translated at 100.0% (16 of 16 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/pl/ --- fastlane/metadata/android/pl/changelogs/89.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 fastlane/metadata/android/pl/changelogs/89.txt diff --git a/fastlane/metadata/android/pl/changelogs/89.txt b/fastlane/metadata/android/pl/changelogs/89.txt new file mode 100644 index 00000000..edfc6f0a --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Otwórz jako..." teraz jest także dostępne w menu na profilach kont gdy używane jest kilka kont +- Login teraz jest obsługiwany w WebView w aplikacji +- Wsparcie dla Androida 12 +- Wsparcie nowego API konfiguracji instancji Mastodon +- i wiele innych małych poprawek i ulepszeń From 90ff6544734bf49f00d89ea610346ef0bfc4672e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Sun, 1 May 2022 17:40:32 +0000 Subject: [PATCH 064/104] Translated using Weblate (Icelandic) Currently translated at 100.0% (16 of 16 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/is/ --- fastlane/metadata/android/is/changelogs/89.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 fastlane/metadata/android/is/changelogs/89.txt diff --git a/fastlane/metadata/android/is/changelogs/89.txt b/fastlane/metadata/android/is/changelogs/89.txt new file mode 100644 index 00000000..e379fc72 --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky útg.17.0 + +- "Opna sem..." er núna líka á valmyndinni í notendasniðum þegar verið er að nota marga aðganga +- Innskráning er núna meðhöndluð í WebView innan forritsins +- Stuðningur við Android 12 +- Stuðningur við API-kerfisviðmót fyrir nýja uppsetningu Mastodon-tilvika +- og mökkur af smærri endurbótum og lagfæringum From a86391a7e928922ff726acb333de1212fd00c552 Mon Sep 17 00:00:00 2001 From: Bruno Miguel Date: Sun, 1 May 2022 17:40:32 +0000 Subject: [PATCH 065/104] Translated using Weblate (Portuguese (Portugal)) Currently translated at 100.0% (16 of 16 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/pt_PT/ --- fastlane/metadata/android/pt-PT/changelogs/58.txt | 12 ++++++++++++ fastlane/metadata/android/pt-PT/changelogs/61.txt | 7 +++++++ fastlane/metadata/android/pt-PT/changelogs/67.txt | 9 +++++++++ fastlane/metadata/android/pt-PT/changelogs/68.txt | 3 +++ fastlane/metadata/android/pt-PT/changelogs/70.txt | 8 ++++++++ fastlane/metadata/android/pt-PT/changelogs/72.txt | 11 +++++++++++ fastlane/metadata/android/pt-PT/changelogs/74.txt | 8 ++++++++ fastlane/metadata/android/pt-PT/changelogs/77.txt | 10 ++++++++++ fastlane/metadata/android/pt-PT/changelogs/80.txt | 7 +++++++ fastlane/metadata/android/pt-PT/changelogs/82.txt | 5 +++++ fastlane/metadata/android/pt-PT/changelogs/83.txt | 3 +++ fastlane/metadata/android/pt-PT/changelogs/87.txt | 8 ++++++++ fastlane/metadata/android/pt-PT/changelogs/89.txt | 7 +++++++ fastlane/metadata/android/pt-PT/full_description.txt | 12 ++++++++++++ .../metadata/android/pt-PT/short_description.txt | 1 + fastlane/metadata/android/pt-PT/title.txt | 1 + 16 files changed, 112 insertions(+) create mode 100644 fastlane/metadata/android/pt-PT/changelogs/58.txt create mode 100644 fastlane/metadata/android/pt-PT/changelogs/61.txt create mode 100644 fastlane/metadata/android/pt-PT/changelogs/67.txt create mode 100644 fastlane/metadata/android/pt-PT/changelogs/68.txt create mode 100644 fastlane/metadata/android/pt-PT/changelogs/70.txt create mode 100644 fastlane/metadata/android/pt-PT/changelogs/72.txt create mode 100644 fastlane/metadata/android/pt-PT/changelogs/74.txt create mode 100644 fastlane/metadata/android/pt-PT/changelogs/77.txt create mode 100644 fastlane/metadata/android/pt-PT/changelogs/80.txt create mode 100644 fastlane/metadata/android/pt-PT/changelogs/82.txt create mode 100644 fastlane/metadata/android/pt-PT/changelogs/83.txt create mode 100644 fastlane/metadata/android/pt-PT/changelogs/87.txt create mode 100644 fastlane/metadata/android/pt-PT/changelogs/89.txt create mode 100644 fastlane/metadata/android/pt-PT/full_description.txt create mode 100644 fastlane/metadata/android/pt-PT/short_description.txt create mode 100644 fastlane/metadata/android/pt-PT/title.txt diff --git a/fastlane/metadata/android/pt-PT/changelogs/58.txt b/fastlane/metadata/android/pt-PT/changelogs/58.txt new file mode 100644 index 00000000..24aad2f2 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/58.txt @@ -0,0 +1,12 @@ +Tusky v6.0 + +- Os filtros de timeline passaram para "Preferências da Conta" e sincronizam com servidor +- Pode ter uma hashtag personalizada como separador +- Suporte a edição de listas +- O editor sugere emojis personalizados ao escrever +- Nova configuração: "seguir tema do sistema" +- Melhor acessibilidade da timeline +- O Tusky ignora notificações desconhecidas, deixando de crashar +- Nova opção: trocar o idioma do sistema por outro +- Novas traduções +- Muitas outras melhorias e correções diff --git a/fastlane/metadata/android/pt-PT/changelogs/61.txt b/fastlane/metadata/android/pt-PT/changelogs/61.txt new file mode 100644 index 00000000..3cc7097e --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Suporte para mostragem de votações, para votação e notificação de votações +- Botões novos para filtrar notificações e excluí-las +- Exclua e rascunhe os seus toots +- Novo indicador que mostra, na foto de perfil, se uma conta é um bot (pode ser desativado nas preferências) +- Novas traduções: Norueguês, Bokmål e Esloveno. diff --git a/fastlane/metadata/android/pt-PT/changelogs/67.txt b/fastlane/metadata/android/pt-PT/changelogs/67.txt new file mode 100644 index 00000000..5d3d3849 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Agora pode criar votações no Tusky +- Pesquisa melhorada +- Nova opção em "Preferências da Conta": "Expandir sempre os toots com Aviso de Conteúdo" +- Avatars em formato quadrado com cantos arredondados +- Agora é possível denunciar utilizadores, mesmo que não tenham toots +- O Tusky vai recusar a ligação através de ligações simples (não encriptadas) em Android 6+ +- Muitas outras pequenas melhorias e correções de bugs diff --git a/fastlane/metadata/android/pt-PT/changelogs/68.txt b/fastlane/metadata/android/pt-PT/changelogs/68.txt new file mode 100644 index 00000000..91792113 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Esta atualização garante compatibilidade com Mastodon 3 e melhora a performance e estabilidade. diff --git a/fastlane/metadata/android/pt-PT/changelogs/70.txt b/fastlane/metadata/android/pt-PT/changelogs/70.txt new file mode 100644 index 00000000..6ac528b1 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Agora é possível adicionar toots aos favoritos e ver a lista de favoritos no Tusky. +- Já pode agendar toots, no entanto é necessário agendá-los para pelo menos 5 minutos depois do momento da escrita. +- Já pode adicionar listas na barra lateral do Tusky! +- Já pode partilhar ficheiros de som nos teus toots! + +E muitas outras pequenas melhorias e correções de bugs! diff --git a/fastlane/metadata/android/pt-PT/changelogs/72.txt b/fastlane/metadata/android/pt-PT/changelogs/72.txt new file mode 100644 index 00000000..f42b0a8e --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Notificações de seguidores pendentes quando a conta está trancada! +- Novas funcionalidades nas "Preferências": + * desativação do gesto que alterna entre separadores + * diálogo de confirmação antes de dar boost + * mostragem da pré-visualização de links nas timelines +- As conversas agora podem ser silenciadas +- As votações passam a ser calculadas pelo número de votantes e não pelo número de votos +- Várias correções relacionadas com a escrita de toots + - Traduções melhoradas diff --git a/fastlane/metadata/android/pt-PT/changelogs/74.txt b/fastlane/metadata/android/pt-PT/changelogs/74.txt new file mode 100644 index 00000000..9595cb1f --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v.12.0 + +- Interface principal melhorada - passa a ser possível mover os separadores para baixo! +- Ao silenciar um utilizador, pode também escolher se também pretende silenciar as notificações +- Agora dá para seguir quantas hashtags quiser num único separador! +- A exibição da descrição dos conteúdos multimédia foi melhorada para suportar descrições super longas + +Registo completo de alterações: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/pt-PT/changelogs/77.txt b/fastlane/metadata/android/pt-PT/changelogs/77.txt new file mode 100644 index 00000000..01c57b33 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- Suporte para anotações em perfis (novidade do Mastodon 3.2.0) +- Suporte para anúncios do(s) administrador(es) de instâncias (novidade do Mastodon 3.1.0) + +- O avatar da sua conta selecionada passa a ficar visível na barra de ferramentas principal (canto superior esquerdo) +- Tocar no nome de utilizador na timeline abrirá o perfil em questão + +- Várias pequenas melhorias e correções +- Traduções melhoradas diff --git a/fastlane/metadata/android/pt-PT/changelogs/80.txt b/fastlane/metadata/android/pt-PT/changelogs/80.txt new file mode 100644 index 00000000..866dc8e5 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Receba notificações quando um utilizador que segue publicar um toot - basta clicar no ícone do sino (novidade do Mastodon 3.3.0) +- O suporte para rascunhos do Tusky foi reescrito para ser mais rápido, simples e menos propenso a erros. +- Foi adicionado uma funcionalidade de bem-estar, que permite limitar algumas funcionalidades no Tusky. +- O Tusky já consegue animar os emojis personalizados +Registo completo de alterações: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/pt-PT/changelogs/82.txt b/fastlane/metadata/android/pt-PT/changelogs/82.txt new file mode 100644 index 00000000..0ee9e876 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- O menu principal passa a mostrar uma opção para ver os utilizadores que pediram para o seguir! +- O relógio para agendar toots ganhou um aspeto mais consistente com o resto do Tusky +Registo completo de alterações: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/pt-PT/changelogs/83.txt b/fastlane/metadata/android/pt-PT/changelogs/83.txt new file mode 100644 index 00000000..4c71e64d --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +O Tusky já não crasha ao adicionar descrição às imagens diff --git a/fastlane/metadata/android/pt-PT/changelogs/87.txt b/fastlane/metadata/android/pt-PT/changelogs/87.txt new file mode 100644 index 00000000..79a81157 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- O algoritmo de carregamento da timeline foi completamente reescrito para ser mais rápida, mais estável e mais fácil de manter. +- O Tusky passa a poder animar emojis personalizados no formato APNG & WebP Animated. +- Muitas correções de bugs +- Suporte para Android 11 +- Novas traduções: gaélico escocês, galego, ucraniano +- Traduções melhoradas diff --git a/fastlane/metadata/android/pt-PT/changelogs/89.txt b/fastlane/metadata/android/pt-PT/changelogs/89.txt new file mode 100644 index 00000000..28cebc1b --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Abrir como..." está disponível no menu de perfis de contas quando estão várias contas configuradas +- O login passa a ser feito numa WebView dentro da aplicação +- Suporte para Android 12 +- Suporte para a nova API de configuração de instâncias do Mastodon +- Várias pequenas melhorias e correções diff --git a/fastlane/metadata/android/pt-PT/full_description.txt b/fastlane/metadata/android/pt-PT/full_description.txt new file mode 100644 index 00000000..52d67d81 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/full_description.txt @@ -0,0 +1,12 @@ +Tusky é um cliente leve para Mastodon, um servidor de rede social de código aberto e livre. + +• Design Material +• Maioria das APIs do Mastodon implementadas +• Suporte para várias contas +• Temas diurno e noturno, com possibilidade de troca automática de acordo com o horário +• Rascunhos - Escreva os seus toots e guarde-os para mais tarde +• Escolha entre estilos diferentes de emoji +• Otimizado para todos os tamanhos de ecrã +• Código totalmente aberto, sem dependências não-livres como Google Play Services + +Para ler mais sobre o Mastodon, visite o endereço https://joinmastodon.org/ diff --git a/fastlane/metadata/android/pt-PT/short_description.txt b/fastlane/metadata/android/pt-PT/short_description.txt new file mode 100644 index 00000000..38a439d8 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/short_description.txt @@ -0,0 +1 @@ +Um cliente multi-contas para a rede social Mastodon diff --git a/fastlane/metadata/android/pt-PT/title.txt b/fastlane/metadata/android/pt-PT/title.txt new file mode 100644 index 00000000..0238ffc0 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/title.txt @@ -0,0 +1 @@ +Tusky From 76accf0d0df051c3e4e1925494b26e49ed553125 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Tue, 3 May 2022 17:12:40 +0000 Subject: [PATCH 066/104] Translated using Weblate (French) Currently translated at 100.0% (477 of 477 strings) Translated using Weblate (Arabic) Currently translated at 98.3% (469 of 477 strings) Co-authored-by: ButterflyOfFire Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ar/ Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fr/ Translation: Tusky/Tusky --- app/src/main/res/values-ar/strings.xml | 6 ++++++ app/src/main/res/values-fr/strings.xml | 1 + 2 files changed, 7 insertions(+) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index cb693670..d0160a60 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -551,4 +551,10 @@ 180 يومًا 365 يومًا تحرير منشور + حسابات جديدة + لِج + قام %s بإنشاء حساب + أحدهم أنشأ حسابا جديدا + منشورات تم تعديلها + قام %s بتعديل منشوره \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 37f8b984..dc30483b 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -547,4 +547,5 @@ un message avec lequel j\'ai interagi est modifié Messages modifiés Notifications quand un post avec lequel vous avez interagi est modifié + Se connecter \ No newline at end of file From 97e8a25a47aadd9da09d1bd22a6ae80dd7f424aa Mon Sep 17 00:00:00 2001 From: mondstern Date: Tue, 3 May 2022 17:12:41 +0000 Subject: [PATCH 067/104] Translated using Weblate (German) Currently translated at 99.1% (473 of 477 strings) Co-authored-by: mondstern Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/ Translation: Tusky/Tusky --- app/src/main/res/values-de/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 254a6116..b05ccc96 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -534,4 +534,5 @@ %s hat sich registriert Jemand hat sich registriert Benachrichtigungen, wenn Beiträge bearbeitet werden, mit denen du interagiert hast + Anmelden \ No newline at end of file From 95581fa0269b7872bc9fa23a9fe1ee2af814d04c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Tue, 3 May 2022 17:12:41 +0000 Subject: [PATCH 068/104] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (477 of 477 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 1ec9ef80..5219775d 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -515,4 +515,5 @@ khi một tút mà tôi tương tác bị sửa Sửa tút Thông báo khi tút mà tôi tương tác bị sửa + Đăng nhập \ No newline at end of file From 58bd20edcae2c95c77755942d5aaf718271befa2 Mon Sep 17 00:00:00 2001 From: Makis Diakatos Date: Tue, 3 May 2022 17:12:41 +0000 Subject: [PATCH 069/104] Translated using Weblate (Greek) Currently translated at 22.0% (105 of 477 strings) Co-authored-by: Makis Diakatos Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/el/ Translation: Tusky/Tusky --- app/src/main/res/values-el/strings.xml | 102 +++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 4734fea9..85583a2b 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -3,4 +3,106 @@ Αυτό δεν μπορεί να είναι κενό. Προέκυψε σφάλμα δικτύου! Παρακαλώ ελέγξτε τη σύνδεσή σας και προσπαθήστε ξανά! Προέκυψε ένα σφάλμα. + Αποκλεισμένοι χρήστες + Ακύρωση αιτήματος ακολούθησης; + Διαγραφή αυτής της συζήτησης; + Δεν υπάρχουν αποτελέσματα + Επεργασία προφίλ + Ακολουθεί + Επαναφορά + ο/η %s σας ακολούθησε + Χρήστες σε σίγαση + Αποσύνδεση + Μην ακολουθείτε + Άρση σίγασης του %s + Διαγραφή και αναδιατύπωση + Επεξεργασία προφίλ + Κοινοποίηση + Άδειες + Ανοίξτε σε browser + Αιτήματα ακολούθησης + Προσθήκη σελιδοδείκτη + Περισσότερα + Σελιδοδείκτες + Σελιδοδείκτες + Ακόλουθοι + Άρση αποκλεισμού + Αγαπημένα + Η δημοσίευση είναι πολύ μεγάλη! + Πληκτρολόγιο emoji + Είστε σίγουροι ότι θέλετε να αποσυνδεθείτε από τον λογαριασμό %1$s; + Προσχέδια + Αγαπημένα + Απάντηση… + Απόρριψη + Αποκλεισμένοι χρήστες + Αφαίρεση προώθησης + Επεξεργασία + Σίγαση του %s + Αποκλεισμός + Αναίρεση + ο/η %s ζήτησε να σας ακολουθήσει + Απάντηση + Καρτέλες + ο/η %s προώθησε τη δημοσίευσή σας + στον/στην %s άρεσε η δημοσίευσή σας + Ακολουθήστε + Αναφορά + Σίγαση + Τα μουσικά αρχεία πρέπει να είναι μικρότερα από 40MB. + Αφαίρεση αγαπημένου + Αναφορά του/της %s + Προτιμήσεις Λογαριασμού + Προσθήκη καρτέλας + Αντιγραφή συνδέσμου + Αναζήτηση… + Αποδοχή + Εμφάνιση προωθήσεων + Προφίλ + Αιτήματα ακολούθησης + Αναζήτηση + Διαγραφή συζήτησης + Διαγραφή + ο/η %s μόλις δημοσίευσε + Αποθήκευση + Γρήγορη Απάντηση + Χρήστες σε σίγαση + Το αρχείο πρέπει να είναι μικρότερο από 8MB. + Απόκρυψη προωθήσεων + Προτιμήσεις + Σύνδεση + Ανακοινώσεις + Προσχέδια + ο/η %s έκανε εγγραφή + Προσπαθήστε ξανά + Διαγραφή αυτής της δημοσίευσης; + Άρση σίγασης + Αγαπημένο + Σύνδεσμοι + Κλείσιμο + Ειδοποιήσεις + Γράψτε + Σύνδεση με Mastodon + Επεξεργασία + Προώθηση + Άρση ακολούθησης αυτού του λογαριασμού; + Απόκρυψη ειδοποιήσεων + Αφαίρεση σελιδοδείκτη + Προειδοποίηση περιεχομένου + Σύνδεσμοι + Σύνδεση… + Προγραμματισμένες δημοσιεύσεις + Προγραμματισμός δημοσίευσης + Προγραμματισμένες δημοσιεύσεις + Δημοσιεύσεις + Καρφιτσωμένο + Ευαίσθητο περιεχόμενο + Κρυμμένα μέσα + ο/η %s το προώθησε + Με απαντήσεις + Δείτε περισσότερα + Δείτε λιγότερα + Κλικ για να δείτε + ο/η %s επεξεργάστηκε τη δημοσίευσή του/της + Διαγραφή και αναδιατύπωση αυτής της δημοσίευσης; \ No newline at end of file From 7b98adf1eed0f1b939c0f33afc159993a261a76e Mon Sep 17 00:00:00 2001 From: Bruno Miguel Date: Tue, 3 May 2022 17:12:41 +0000 Subject: [PATCH 070/104] Translated using Weblate (Portuguese (Portugal)) Currently translated at 100.0% (477 of 477 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 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index e501ed2a..aa81160b 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -339,7 +339,7 @@ Descrição para deficientes visuais \n(até %d caracteres) - + Descrever Remover From cb1f4898ba0d8815f0406e0bec7954faba0ffa30 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Tue, 3 May 2022 17:12:41 +0000 Subject: [PATCH 071/104] Translated using Weblate (Italian) Currently translated at 100.0% (477 of 477 strings) Translated using Weblate (Italian) Currently translated at 96.6% (461 of 477 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 | 297 +++++++++++++------------ 1 file changed, 152 insertions(+), 145 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b1a3e130..1a15d152 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -2,33 +2,33 @@ Si è verificato un errore. Si è verificato un errore di rete! Per favore controlla la tua connessione e riprova! - Questo non può esser vuoto. - Inserito un dominio non valido - Autenticazione fallita con quell\'istanza. - Non riesco a trovare un browser web da usare. + Questo non può essere vuoto. + Inserito dominio non valido + Autenticazione con quell\'istanza fallita. + Nessun browser web utilizzabile trovato. Si è verificato un errore di autenticazione non identificato. - L\'autorizzazione è stata negata. - Errore nell\'acquisizione del token di accesso. - Lo stato è troppo lungo! - La dimensione dei file immagine deve essere inferiore a 8 MB. - La dimensione dei file video deve essere inferiore a 40 MB. - Questo tipo di file non può essere caricato. - Questo file non può essere aperto. - Il permesso di lettura della scheda sd è richiesto. - È richiesta l\'autorizzazione di archiviazione. - Immagini e video non possono essere allegati allo stesso stato. - Il caricamento non è riuscito. - Errore nell\'invio del toot. + 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. + È richiesto il permesso di leggere file. + È richiesto il permesso di salvare file. + Non è possibile allegare allo stesso post immagini e video. + Il caricamento è fallito. + Errore nell\'invio del post. Home Notifiche Locale Federata - Messaggi Diretti + Messaggi diretti Schede - Toot + Conversazione Post Con risposte - Fissati in alto + Fissati Seguiti Seguono Preferiti @@ -43,15 +43,15 @@ Contenuto sensibile Media nascosto Clicca per visualizzare - Mostra di Più - Mostra Meno + Mostra di più + Mostra di meno Espandi Riduci - Qui non c\'è niente. - Qui non c\'è niente. Trascina verso il basso per aggiornare! - %s ha boostato il tuo toot - %s ha messo il tuo toot nei preferiti - %s ti segue + Qui non c\'è nulla. + Qui non c\'è nulla. Trascina verso il basso per aggiornare! + %s ha boostato il tuo post + %s ha messo il tuo post nei preferiti + %s ti ha seguito Segnala @%s Commenti aggiuntivi? Risposta veloce @@ -79,7 +79,7 @@ Chiudi Profilo Preferenze - Preferenze Account + Preferenze account Preferiti Utenti silenziati Utenti bloccati @@ -102,14 +102,14 @@ Rifiuta Cerca Bozze - Visibilità dei toot - Avviso per il contenuto + Visibilità dei post + Avviso di contenuto sensibile Tastiera emoji - Aggiungi Scheda + Aggiungi scheda Collegamenti Menzioni Hashtag - Apri autore del boost + Vai all\'autore del boost Mostra boost Mostra preferiti Hashtag @@ -117,11 +117,11 @@ Collegamenti Apri media #%d Scaricando %1$s - Copia il link + Copia link Apri come %s Condividi come … - Condividi URL del toot su… - Condividi toot su… + Condividi URL del post su… + Condividi post su… Condividi media su… Inviato! Utente sbloccato @@ -132,7 +132,7 @@ Cosa succede? Avviso di contenuto sensibile Mostra nome - Bio + Biografia Cerca… Nessun risultato Rispondi… @@ -152,22 +152,22 @@ Scarica Revocare la richiesta di seguire? Smettere di seguire questo account? - Eliminare questo toot? + Eliminare questo post\? Pubblico: visibile sulla timeline pubblica - Non Elencato: non visibile sulla timeline pubblica e locale - Solo Follower: visibile solo dai tuoi follower + Non in elenco: non visibile sulla timeline pubblica e locale + Solo follower: visibile solo dai tuoi follower Diretto: visibile solo agli utenti menzionati - Modifica Notifiche + Notifiche Notifiche Allarmi Notifica con suoneria Notifica con vibrazione Notifica con luce Notificami quando - sono stato menzionato - sono stato seguito - i miei post sono boostati - i miei post sono messi nei preferiti + vengo menzionato + vengo seguito + i miei post vengono boostati + i miei post vengono messi nei preferiti Aspetto Tema dell\'app Timeline @@ -176,10 +176,10 @@ Chiaro Nero Automatico al tramonto - Usa Tema di Sistema + Usa tema di sistema Browser - Usa Tab Personalizzate di Chrome - Nascondi il pulsante componi mentre scorri + Usa Custom Tabs di Chrome + Nascondi il pulsante Componi mentre scorri Lingua Filtraggio della timeline Schede @@ -193,25 +193,25 @@ Porta proxy HTTP Privacy di default dei post Segna sempre media come contenuto sensibile - Pubblicando (sincronizzato con il server) + Pubblicazione (sincronizzato con il server) Sincronizzazione delle impostazioni fallita Pubblico - Non elencato - Solo per chi ti segue - Dimensione del testo degli stati + Non in elenco + Solo follower + Dimensione del testo dei post Piccolissimo Piccolo Normale Grande Grandissimo - Nuove Menzioni - Notifiche quando qualcuno ti menziona - Nuove persone che ti seguono - Notifiche su nuove persone che ti seguono + Nuove menzioni + Notifiche di quando vieni menzionato da qualcuno + Nuovi follower + Notifiche su nuovi follower Boost - Notifiche quando i tuoi toot vengono boostati + Notifiche sui tuoi post che vengono boostati Preferiti - Notifiche quando i tuoi toot vengono segnati come preferiti + Notifiche sui tuoi post 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 @@ -234,14 +234,14 @@ --> Sito web del progetto:\n https://tusky.app - Segnala problemi & richiedi funzionalità:\n - https://github.com/tuskyapp/Tusky/issues + Segnala problemi e richiedi funzionalità: +\n https://github.com/tuskyapp/Tusky/issues Profilo di Tusky - Condividi contenuto del toot - Condividi link al toot + Condividi contenuto del post + Condividi link al post Immagini Video - In attesa di approvazione + Richiesta inviata in %d a in %dg @@ -250,14 +250,14 @@ in %ds %da %dg - %d o - %d min - %d s - Seguono te - Mostra sempre tutto il contenuto sensibile + %do + %dmin + %ds + Ti segue + Mostra sempre tutti i contenuti sensibili Media Rispondendo a @%s - carica di più + carica altri Timeline pubbliche Conversazioni Aggiungi filtro @@ -265,7 +265,7 @@ Rimuovi Aggiorna Frase da filtrare - Aggiungi Account + Aggiungi account Aggiungi un nuovo Account Mastodon Liste Liste @@ -288,29 +288,29 @@ Inserisci descrizione Rimuovi Blocca account - Richiede la tua approvazione manuale di chi ti segue + Richiedi una tua approvazione manuale per seguirti Salvare bozza? - Inviando il Toot… + Inviando il post… Errore durante l\'invio - Invio Toot + Invio post Invio annullato - Una copia del toot è stata salvata nelle tue bozze + Una copia del post è stata salvata nelle tue bozze Componi La tua istanza %s non ha nessuna emoji personalizzata - Stile di emoji - Predefiniti del sistema + Stile delle emoji + Predefinite del sistema Dovrai prima scaricare questo pacchetto di emoji - Eseguendo una ricerca… - Espandi/Riduci tutti gli stati - Apri toot + Ricerca in corso… + Espandi/riduci tutti i post + Apri post Riavvio dell\'app richiesto Devi riavviare Tusky per applicare queste modifiche Più tardi Riavvia Le emoji predefinite del tuo dispositivo - Le emoji Blob conosciute da Android 4.4-7.1 + Le emoji Blob di Android 4.4-7.1 Le emoji standard di Mastodon - Scaricamento fallito + Download fallito Bot %1$s si è spostato su: Boost con la visibilità del post di origine @@ -323,63 +323,60 @@ aggiungi dati Etichetta Contenuto - Usa tempo assoluto + Usa ora assoluta Il profilo dell\'utente mostrato qui sotto potrebbe essere incompleto. Premi per aprire il profilo completo nel browser. - Non fissare + Smetti di fissare Fissa - %1$s Mi piace - %1$s Mi piace + %1$s Preferito + %1$s Preferiti <b>%s</b> Boost <b>%s</b> Boost Boostato da - Preferito da + Aggiunto ai preferiti da %1$s %1$s e %2$s %1$s, %2$s ed altri %3$d - limite massimo di %1$d tab raggiunto - limite massimo di %1$d tab raggiunto + limite massimo di %1$d scheda raggiunto + limite massimo di %1$d schede raggiunto - Media: %s - + Media: %s Contenuto sensibile: %s Nessuna descrizione Ribloggato - Apprezzato - + Messo nei preferiti Pubblico - Non elencato - - Seguaci + Non in elenco + Solo follower Diretti Nome della lista Scarica media Scaricando media - Componi Toot + Componi post Hashtag senza # Componi - Pulisci + Svuota Filtra Applica - Mostra indicatore per bot + Mostra indicatore bot Sei sicuro di voler permanentemente eliminare tutte le tue notifiche\? Cancella e riscrivi - Cancellare e riscrivere questo toot\? + Cancellare e riscrivere questo post\? %s voto %s voti - termina alle %s - terminato + si conclude alle %s + concluso Vota Domini nascosti Domini nascosti @@ -387,37 +384,37 @@ %s mostrati 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 - Le votazioni sono finite - Mostra le animazioni delle GIF negli avatar + dei sondaggi si sono conclusi + Riproduci animazioni avatar Votazioni - Notifiche sulle votazioni che sono concluse + Notifiche sulle votazioni che si sono concluse Parola intera Quando la parola chiave o la frase sono composte da soli caratteri alfanumerici, sarà applicata solo se corrisponde alla parola completa - Insieme di emoji di Google + Set di emoji di Google Segnalibri Segnalibro Modifica Segnalibri Aggiungi sondaggio - Fatto con Tusky - Espandi sempre i toot segnalati come contenuto sensibile - Messo nei segalibri + Fatto usando Tusky + Espandi sempre i post segnalati come contenuto sensibile + Messo nei segnalibri Sondaggio con scelte: %1$s, %2$s, %3$s, %4$s; %5$s Scegli lista Lista Azioni per l\'immagine %s - Un sondaggio che hai votato è terminato - Un sondaggio che hai creato è terminato + Un sondaggio che hai votato si è concluso + Un sondaggio che hai creato si è concluso - %d giorno rimasti + %d giorno rimasto %d giorni rimasti - %d ora rimasti + %d ora rimasta %d ore rimasti - %d minuto rimasti + %d minuto rimasto %d minuti rimasti @@ -427,12 +424,12 @@ Continua Indietro Fatto - Inviato con successo @%s + Segnalato @%s con successo Altri commenti Inoltra a %s - Errore durante l\'invio - Errore durante lo scaricamento degli aggiornamenti - La segnalazione sarà inviata al moderatore del tuo server. Puoi spiegare perchè vuoi segnalare questo utente qui sotto: + Segnalazione fallita + Scaricamento dei post fallito + La segnalazione sarà inviata al moderatore del tuo server. Puoi spiegare perchè stai segnalando questo utente qui sotto: L\'utente è su un altro server. Mandare una copia della segnalazione anche lì\? Utenti Errore durante la ricerca @@ -450,10 +447,10 @@ Scelta %d Modifica Errore nella ricerca del post %s - Toot programmati - Toot programmati - Programma un toot - RIpristina + Post programmati + Post programmati + Programma un post + Ripristina %1$s • %2$s Non hai bozze. @@ -464,66 +461,66 @@ Aggiungi hashtag Silenziare @%s\? Bloccare @%s\? - Non silenziare più %s - Smetti di silenziare conversazione + Smetti di silenziare %s + Smetti di silenziare la conversazione Silenzia conversazione %s ha chiesto di seguirti - La dimensione dei file audio deve essere inferiore a 40 MB. + I file audio devono essere più piccoli di 40 MB. Smetti di silenziare %s Richieste di seguirti Salvato! La tua nota privata su questo account Nascondi il titolo della barra degli strumenti in alto - Mostra la finestra di dialogo di conferma prima del boosting + Mostra la finestra di conferma prima di boostare Mostra le anteprime dei collegamenti nelle timelines - Mastodon ha un intervallo minimo di programmazione di 5 minuti. + Mastodon ha un intervallo di programmazione minimo di 5 minuti. Non ci sono annunci. - Non hai stati pianificati. + Non hai post pianificati. Abilita il gesto di scorrimento per passare da una scheda all\'altra Notifiche sulle richieste di essere seguiti - Parte inferiore + In fondo In cima - Posizione di navigazione principale - Mostra sfumature colorate per i media nascosti + Posizione barra di navigazione principale + Mostra gradienti colorati per i media nascosti Nascondi notifiche Disattiva le notifiche da %s Riattiva le notifiche da %s Annunci - Richieste di seguirti + mi viene richiesto di seguirmi Nascondi statistiche quantitative sui profili Nascondi le statistiche quantitative sui post - Limita le notifiche della timeline - Revisiona le notifiche + Limita le notifiche dalla timeline + Rivedi le notifiche Benessere - Notifiche di quando qualcuno a cui sei iscritto ha pubblicato un nuovo toot - Nuovi toots - qualcuno a cui sono iscritto ha pubblicato un nuovo toot - %s appena pubblicato + Notifiche di nuovi post di qualcuno a cui sei iscritto + Nuovi post + qualcuno che seguo ha pubblicato un nuovo post + %s ha appena pubblicato Non puoi caricare più di %1$d allegato multimediale. Non puoi caricare più di %1$d allegati multimediali. - Il toot a cui hai scritto una risposta è stato rimosso - Bozza cancellata - L\'invio di questo toot è fallito! + Il post a cui hai scritto una risposta è stato rimosso + Bozza eliminata + L\'invio di questo post è fallito! Sei sicuro di voler cancellare la lista %s\? Indefinita Durata Allegati Audio - Mostra le animazioni delle emojis personalizzate + Riproduci emoji animate Iscriviti Rimuovere questa conversazione\? - Errore nel recuperare le informazioni sulla risposta + Errore nel recupero delle informazioni sulla risposta Disiscriviti - Rimuovi conversazione + Elimina conversazione Alcune informazioni che potrebbero influenzare il tuo benessere mentale saranno nascoste. Questo include: \n \n - Notifiche riguardo a Preferiti/Boost/Following -\n - Conteggio dei Preferiti/Boost nei toot -\n - Statistiche riguardo a Preferiti e Post nei profili +\n - Conteggio dei Preferiti/Boost nei post +\n - Statistiche riguardo a Preferiti/Post nei profili \n -\n Le notifiche push non saranno influenzate, ma puoi rivedere le tue impostazioni delle notifiche manualmente. +\n Le notifiche push non saranno influenzate, ma puoi modificare le tue impostazioni delle notifiche manualmente. Rimuovi segnalibro Chiedi conferma prima di boostare 14 giorni @@ -532,5 +529,15 @@ 90 giorni 180 giorni 365 giorni - Anche se il tuo account non è bloccato, lo staff di %1$s ha pensato che potresti voler controllare queste richieste di following 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 account manualmente. + %s si è registrato + qualcuno si è registrato + Login + %s ha modificato il suo post + un post con cui ho interagito è stato modificato + Componi post + Registrazioni + Notifiche di quando qualcuno si è registrato + Modifiche ai post + Notifiche di quando i post con cui hai interagito vengono modificati \ No newline at end of file From db81ede04a7309962b1328119448612df6d34c2c Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Wed, 4 May 2022 18:40:29 +0200 Subject: [PATCH 072/104] fix login webview title color with light theme (#2497) --- .../tusky/components/login/LoginWebViewActivity.kt | 4 ++-- .../layout/{login_webview.xml => activity_login_webview.xml} | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) rename app/src/main/res/layout/{login_webview.xml => activity_login_webview.xml} (88%) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt index a32a164c..2ed38720 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt @@ -20,7 +20,7 @@ import androidx.core.net.toUri import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.R -import com.keylesspalace.tusky.databinding.LoginWebviewBinding +import com.keylesspalace.tusky.databinding.ActivityLoginWebviewBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.viewBinding @@ -78,7 +78,7 @@ sealed class LoginResult : Parcelable { /** Activity to do Oauth process using WebView. */ class LoginWebViewActivity : BaseActivity(), Injectable { - private val binding by viewBinding(LoginWebviewBinding::inflate) + private val binding by viewBinding(ActivityLoginWebviewBinding::inflate) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/res/layout/login_webview.xml b/app/src/main/res/layout/activity_login_webview.xml similarity index 88% rename from app/src/main/res/layout/login_webview.xml rename to app/src/main/res/layout/activity_login_webview.xml index 67d47d6f..1630239e 100644 --- a/app/src/main/res/layout/login_webview.xml +++ b/app/src/main/res/layout/activity_login_webview.xml @@ -11,8 +11,7 @@ + android:layout_height="wrap_content" /> From b4eda5ea65e71419233b6a350ade04baf4c95e1f Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Thu, 5 May 2022 18:27:05 +0200 Subject: [PATCH 073/104] Unbreak link previews in timelines (#2506) --- .../timeline/TimelineTypeMappers.kt | 12 ++++--- .../keylesspalace/tusky/db/AppDatabase.java | 9 +++++- .../com/keylesspalace/tusky/db/TimelineDao.kt | 2 +- .../tusky/db/TimelineStatusEntity.kt | 3 +- .../com/keylesspalace/tusky/di/AppModule.kt | 2 +- .../keylesspalace/tusky/db/TimelineDaoTest.kt | 32 +++++++++++++++++-- 6 files changed, 50 insertions(+), 10 deletions(-) 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 6ec95423..12422a95 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 @@ -21,6 +21,7 @@ import com.keylesspalace.tusky.db.TimelineAccountEntity import com.keylesspalace.tusky.db.TimelineStatusEntity import com.keylesspalace.tusky.db.TimelineStatusWithAccount import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Card import com.keylesspalace.tusky.entity.Emoji import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.Poll @@ -96,7 +97,8 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity { expanded = loading, contentCollapsed = false, contentShowing = false, - pinned = false + pinned = false, + card = null, ) } @@ -136,7 +138,8 @@ fun Status.toEntity( expanded = expanded, contentShowing = contentShowing, contentCollapsed = contentCollapsed, - pinned = actionableStatus.pinned == true + pinned = actionableStatus.pinned == true, + card = actionableStatus.card?.let(gson::toJson), ) } @@ -151,6 +154,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { val application = gson.fromJson(status.application, Status.Application::class.java) val emojis: List = gson.fromJson(status.emojis, emojisListType) ?: emptyList() val poll: Poll? = gson.fromJson(status.poll, Poll::class.java) + val card: Card? = gson.fromJson(status.card, Card::class.java) val reblog = status.reblogServerId?.let { id -> Status( @@ -178,7 +182,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { pinned = false, muted = status.muted, poll = poll, - card = null + card = card, ) } val status = if (reblog != null) { @@ -235,7 +239,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { pinned = status.pinned, muted = status.muted, poll = poll, - card = null + card = card, ) } 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 293db65e..d5f023e5 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 = 34) + }, version = 35) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -534,4 +534,11 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsUpdates` INTEGER NOT NULL DEFAULT 1"); } }; + + public static final Migration MIGRATION_34_35 = new Migration(34, 35) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `card` TEXT"); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt index dd59f2a3..2c6ef188 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt @@ -36,7 +36,7 @@ SELECT s.serverId, s.url, s.timelineUserId, s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt, s.emojis, s.reblogsCount, s.favouritesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive, s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId, -s.content, s.attachments, s.poll, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned, +s.content, s.attachments, s.poll, s.card, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned, a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId', a.localUsername as 'a_localUsername', a.username as 'a_username', a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar', 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 41b122c3..2c4d45c3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt @@ -78,7 +78,8 @@ data class TimelineStatusEntity( val expanded: Boolean, // used as the "loading" attribute when this TimelineStatusEntity is a placeholder val contentCollapsed: Boolean, val contentShowing: Boolean, - val pinned: Boolean + val pinned: Boolean, + val card: 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 c92d52ef..0861e9cf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -63,7 +63,7 @@ class AppModule { AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")), AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29, AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32, - AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34 + AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35, ) .build() } 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 889e5f98..ed652418 100644 --- a/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt @@ -369,13 +369,36 @@ class TimelineDaoTest { assertEquals("99", timelineDao.getTopPlaceholderId(1)) } + @Test + fun `preview card survives roundtrip`() = runBlocking { + val setOne = makeStatus(statusId = 3, cardUrl = "https://foo.bar") + + for ((status, author, reblogger) in listOf(setOne)) { + timelineDao.insertAccount(author) + reblogger?.let { + timelineDao.insertAccount(it) + } + timelineDao.insertStatus(status) + } + + val pagingSource = timelineDao.getStatuses(setOne.first.timelineUserId) + + val loadResult = pagingSource.load(PagingSource.LoadParams.Refresh(null, 2, false)) + + val loadedStatuses = (loadResult as PagingSource.LoadResult.Page).data + + assertEquals(1, loadedStatuses.size) + assertStatuses(listOf(setOne), loadedStatuses) + } + private fun makeStatus( accountId: Long = 1, statusId: Long = 10, reblog: Boolean = false, createdAt: Long = statusId, authorServerId: String = "20", - domain: String = "mastodon.example" + domain: String = "mastodon.example", + cardUrl: String? = null, ): Triple { val author = TimelineAccountEntity( serverId = authorServerId, @@ -403,6 +426,10 @@ class TimelineDaoTest { ) } else null + val card = when (cardUrl) { + null -> null + else -> "{ url: \"$cardUrl\" }" + } val even = accountId % 2 == 0L val status = TimelineStatusEntity( serverId = statusId.toString(), @@ -433,7 +460,8 @@ class TimelineDaoTest { expanded = false, contentCollapsed = false, contentShowing = true, - pinned = false + pinned = false, + card = card, ) return Triple(status, author, reblogAuthor) } From beaed6b8751e547564e5e40c6cb8f93c7a7df88f Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 9 May 2022 19:39:43 +0200 Subject: [PATCH 074/104] Fix crash when saving redrafted media to drafts (#2502) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix crash when saving draft from redraft * fix crash when saving draft from redraft * replace ... with … --- .../components/compose/ComposeActivity.kt | 15 ++++- .../components/compose/ComposeViewModel.kt | 49 ++++++++------- .../tusky/components/drafts/DraftHelper.kt | 63 ++++++++++++++----- app/src/main/res/values/strings.xml | 1 + 4 files changed, 87 insertions(+), 41 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 32162614..a6e8d677 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 @@ -967,8 +967,19 @@ class ComposeActivity : } private fun saveDraftAndFinish(contentText: String, contentWarning: String) { - viewModel.saveDraft(contentText, contentWarning) - finishWithoutSlideOutAnimation() + lifecycleScope.launch { + val dialog = if (viewModel.shouldShowSaveDraftDialog()) { + ProgressDialog.show( + this@ComposeActivity, null, + getString(R.string.saving_draft), true, false + ) + } else { + null + } + viewModel.saveDraft(contentText, contentWarning) + dialog?.cancel() + finishWithoutSlideOutAnimation() + } } override fun search(token: String): List { 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 7b180532..abf2ff42 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 @@ -220,31 +220,36 @@ class ComposeViewModel @Inject constructor( } } - fun saveDraft(content: String, contentWarning: String) { - viewModelScope.launch { - val mediaUris: MutableList = mutableListOf() - val mediaDescriptions: MutableList = mutableListOf() - media.value.forEach { item -> - mediaUris.add(item.uri.toString()) - mediaDescriptions.add(item.description) - } - - draftHelper.saveDraft( - draftId = draftId, - accountId = accountManager.activeAccount?.id!!, - inReplyToId = inReplyToId, - content = content, - contentWarning = contentWarning, - sensitive = markMediaAsSensitive.value!!, - visibility = statusVisibility.value!!, - mediaUris = mediaUris, - mediaDescriptions = mediaDescriptions, - poll = poll.value, - failedToSend = false - ) + fun shouldShowSaveDraftDialog(): Boolean { + // if any of the media files need to be downloaded first it could take a while, so show a loading dialog + return media.value.any { mediaValue -> + mediaValue.uri.scheme == "https" } } + suspend fun saveDraft(content: String, contentWarning: String) { + val mediaUris: MutableList = mutableListOf() + val mediaDescriptions: MutableList = mutableListOf() + media.value.forEach { item -> + mediaUris.add(item.uri.toString()) + mediaDescriptions.add(item.description) + } + + draftHelper.saveDraft( + draftId = draftId, + accountId = accountManager.activeAccount?.id!!, + inReplyToId = inReplyToId, + content = content, + contentWarning = contentWarning, + sensitive = markMediaAsSensitive.value!!, + visibility = statusVisibility.value!!, + mediaUris = mediaUris, + mediaDescriptions = mediaDescriptions, + poll = poll.value, + failedToSend = false + ) + } + /** * Send status to the server. * Uses current state plus provided arguments. 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 7511dc3c..a6cd3fcd 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 @@ -30,7 +30,12 @@ import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.util.IOUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import okio.buffer +import okio.sink import java.io.File +import java.io.IOException import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -38,6 +43,7 @@ import javax.inject.Inject class DraftHelper @Inject constructor( val context: Context, + val okHttpClient: OkHttpClient, db: AppDatabase ) { @@ -71,11 +77,11 @@ class DraftHelper @Inject constructor( val uris = mediaUris.map { uriString -> uriString.toUri() - }.map { uri -> - if (uri.isNotInFolder(draftDirectory)) { - uri.copyToFolder(draftDirectory) - } else { + }.mapNotNull { uri -> + if (uri.isInFolder(draftDirectory)) { uri + } else { + uri.copyToFolder(draftDirectory) } } @@ -114,6 +120,7 @@ class DraftHelper @Inject constructor( ) draftDao.insertOrReplace(draft) + Log.d("DraftHelper", "saved draft to db") } suspend fun deleteDraftAndAttachments(draftId: Int) { @@ -133,33 +140,55 @@ class DraftHelper @Inject constructor( } } - suspend fun deleteAttachments(draft: DraftEntity) { - withContext(Dispatchers.IO) { - draft.attachments.forEach { attachment -> - if (context.contentResolver.delete(attachment.uri, null, null) == 0) { - Log.e("DraftHelper", "Did not delete file ${attachment.uriString}") - } + suspend fun deleteAttachments(draft: DraftEntity) = withContext(Dispatchers.IO) { + draft.attachments.forEach { attachment -> + if (context.contentResolver.delete(attachment.uri, null, null) == 0) { + Log.e("DraftHelper", "Did not delete file ${attachment.uriString}") } } } - private fun Uri.isNotInFolder(folder: File): Boolean { + private fun Uri.isInFolder(folder: File): Boolean { val filePath = path ?: return true return File(filePath).parentFile == folder } - private fun Uri.copyToFolder(folder: File): Uri { + private fun Uri.copyToFolder(folder: File): Uri? { val contentResolver = context.contentResolver - val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) - val mimeType = contentResolver.getType(this) - val map = MimeTypeMap.getSingleton() - val fileExtension = map.getExtensionFromMimeType(mimeType) + val fileExtension = if (scheme == "https") { + lastPathSegment?.substringAfterLast('.', "tmp") + } else { + val mimeType = contentResolver.getType(this) + val map = MimeTypeMap.getSingleton() + map.getExtensionFromMimeType(mimeType) + } val filename = String.format("Tusky_Draft_Media_%s.%s", timeStamp, fileExtension) val file = File(folder, filename) - IOUtils.copyToFile(contentResolver, this, file) + + if (scheme == "https") { + // saving redrafted media + try { + val request = Request.Builder().url(toString()).build() + + val response = okHttpClient.newCall(request).execute() + + val sink = file.sink().buffer() + + response.body?.source()?.use { input -> + sink.use { output -> + output.writeAll(input) + } + } + } catch (ex: IOException) { + Log.w("DraftHelper", "failed to save media", ex) + return null + } + } else { + IOUtils.copyToFile(contentResolver, this, file) + } return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fc1ed743..d8ce8394 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -640,5 +640,6 @@ Unsubscribe Compose Post + Saving draft… From 45ac280db768cb9bc25a0551d3e0326f56ab317a Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 9 May 2022 19:40:32 +0200 Subject: [PATCH 075/104] Add Portuguese (Portugal) to the in-app language picker (#2507) --- app/src/main/res/values/donottranslate.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index e6f9f62a..ed1e30cb 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -57,6 +57,7 @@ Occitan Polski Português (Brasil) + Português (Portugal) Slovenščina Svenska Taqbaylit @@ -106,6 +107,7 @@ oc pl pt-BR + pt-PT sl sv kab From fc1e153cb3cfe2ede5e07c489ba7dadc7afd3b36 Mon Sep 17 00:00:00 2001 From: idontwanttohaveausername Date: Mon, 9 May 2022 17:39:49 +0000 Subject: [PATCH 076/104] Translated using Weblate (Ukrainian) Currently translated at 100.0% (478 of 478 strings) Co-authored-by: idontwanttohaveausername Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translation: Tusky/Tusky --- app/src/main/res/values-uk/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 7d4b806e..67eb11cb 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -100,7 +100,7 @@ %s надсилає запит на підписку %s підписується на вас Тут нічого немає. Потягніть вниз, щоб оновити! - Тут нічого немає. + Тут пусто. Згорнути Розгорнути Натисніть для перегляду From 8b840396fd2368dc69d660877127c525d759aaa4 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Mon, 9 May 2022 17:39:49 +0000 Subject: [PATCH 077/104] Translated using Weblate (Ukrainian) Currently translated at 100.0% (478 of 478 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (478 of 478 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, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 67eb11cb..6c510097 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -100,7 +100,7 @@ %s надсилає запит на підписку %s підписується на вас Тут нічого немає. Потягніть вниз, щоб оновити! - Тут пусто. + Тут порожньо. Згорнути Розгорнути Натисніть для перегляду @@ -549,4 +549,5 @@ Сповіщення, коли редагується повідомлення, з яким ви взаємодіяли Редакції допису Вхід + Не вдалося завантажити сторінку входу. \ No newline at end of file From 078603819a2a86e557226bf929664b4f9f5832dc Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Mon, 9 May 2022 17:39:49 +0000 Subject: [PATCH 078/104] Translated using Weblate (Italian) Currently translated at 100.0% (478 of 478 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 | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 1a15d152..162846af 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -540,4 +540,5 @@ 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. \ No newline at end of file From 010a4372f9af4b5b3e0e844bd154a98cce834dbd Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 9 May 2022 17:39:49 +0000 Subject: [PATCH 079/104] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (478 of 478 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 5dd1975f..f3d7b2aa 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -535,4 +535,5 @@ 我进行过互动的嘟文被编辑了 嘟文编辑 当你进行过互动的嘟文被编辑时发出通知 + 无法加载登录页。 \ No newline at end of file From aa200404798ee66d3f578a097482b388849bd784 Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Mon, 9 May 2022 17:39:49 +0000 Subject: [PATCH 080/104] =?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% (478 of 478 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 3512b480..80b744bb 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -527,4 +527,5 @@ Redigerte innlegg Varslinger når et innlegg du har hatt en interaksjon med er redigert Innlogging + Klarte ikke å laste innloggingssiden. \ No newline at end of file From 9ec41903cfaeb52e0de38f3401fadfa11969066c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Mon, 9 May 2022 17:39:49 +0000 Subject: [PATCH 081/104] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (478 of 478 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 5219775d..0c8109db 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -516,4 +516,5 @@ Sửa tút Thông báo khi tút mà tôi tương tác bị sửa Đăng nhập + Không thể tải trang đăng nhập. \ No newline at end of file From 529452c1937b3064d0e2c12fc7b29e3c40b3449a Mon Sep 17 00:00:00 2001 From: GunChleoc Date: Mon, 9 May 2022 17:39:49 +0000 Subject: [PATCH 082/104] Translated using Weblate (Gaelic) Currently translated at 100.0% (478 of 478 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 | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 10d64e0c..e27b677a 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -555,4 +555,5 @@ Brathan nuair a thèid postaichean a rinn thu conaltradh leotha a dheasachadh chaidh post a rinn mi conaltradh leis a deasachadh Clàraich a-steach + Cha b’ urrainn dhuinn duilleag a’ chlàraidh a-steach fhosgladh. \ No newline at end of file From d421de6ba8626a0c721c7b9c35e5646bbda81386 Mon Sep 17 00:00:00 2001 From: ruben Date: Mon, 9 May 2022 17:39:49 +0000 Subject: [PATCH 083/104] Translated using Weblate (Catalan) Currently translated at 93.3% (446 of 478 strings) Co-authored-by: ruben Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ca/ Translation: Tusky/Tusky --- app/src/main/res/values-ca/strings.xml | 76 +++++++++++++------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 8b966061..65affe75 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -20,8 +20,8 @@ Notificacions Local Federació - Toot - Posts + Fil + Publicacions Seguits Seguidors Preferits @@ -31,19 +31,19 @@ Edita el perfil Esborranys \@%s - %s tootejat + %s ha impulsat Contingut sensible Fes clic per a visualitzar-lo Mostra\'n més Mostra\'n menys No hi res aquí. Llisca avall per a actualitzar! - %s ha impulsat el teu toot - %s ha marcat com a preferit el teu toot + %s ha impulsat la teva publicació + %s ha marcat com a preferida la teva publicació %s et segueix Denuncia @%s Cap comentari addicional? Respon - Retooteja + Impulsa Preferit Més Escriure @@ -55,8 +55,8 @@ Deixa de blocar Denuncia Elimina - TOOT - TOOT! + PUBLICA + PUBLICA! Torna a intentar-ho Tanca Perfil @@ -83,8 +83,8 @@ Esborranys S\'està baixant %1$s Copia l\'enllaç - Comparteix l\'URL del toot a… - Comparteix el toot a… + Comparteix l\'URL de la publicació a… + Comparteix la publicació a… Enviat! Usuari desblocat Usuari sense silenciar @@ -132,10 +132,10 @@ Amaga el botó de redacció en desplaçament Filtre de la cronologia Pestanyes - Mostra els retoots + Mostra els impulsos Mostra les respostes Mostra les previsualitzacions - Privacitat predeterminada dels toots + Privacitat per defecte de les publicacions Publicació Pública Sense llistar @@ -145,7 +145,7 @@ Notificacions sobre mencions noves Seguidors nous Notificacions sobre nous seguidors - Retoots + Impulsos Notificacions si retootejents els teus toots Preferits Notificacions si marquen com a preferits els teus toots @@ -172,8 +172,8 @@ https://github.com/tuskyapp/Tusky/issues Perfil del Tusky - Comparteix el contingut del toot - Comparteix l\'enllaç al toot + Comparteix el contingut de la publicació + Comparteix l\'enllaç a la publicació Imatges Vídeo @@ -193,7 +193,7 @@ En resposta a @%s carrega\'n més Vota - S\'ha produït un error en enviar el tut. + S\'ha produït un error en publicar. Pestanyes Llicències Amplia @@ -209,11 +209,11 @@ Multimèdia amagada Amaga Estàs segur de tancar la sessió de %1$s\? - Amaga els retoots + Amaga els impulsos Mostra els impulsos Elimina i reecririu Obre el menú - Visibilitat del toot + Visibilitat de la publicació Contingut sensible Afegir una pestanya Enllaços @@ -227,17 +227,17 @@ Baixa el fitxer Compartir la imatge a … Enviat! - S\'ha enviat la petició de seguiment + Petició enviada Amb respostes Teclat d\'emojis Obrir el media #%d - Obrir com %s + Obre com a %s S\'està Descarregant media Resposta enviada correctament. Resposta … Revocar la petició de seguiment\? Vols eliminar aquest toot\? - Esborrar i reescriure aquest toot\? + Vols eliminar i reescriure aquesta publicació\? Finalització de les enquetes Tema Cronologia @@ -267,7 +267,7 @@ Eliminar Afegir un compte Obre l\'autor de l\'impuls - Mostra els retoots + Mostra els impulsos Notificacions d\'enquestes que han finalitzat Línia de temps públiques Actualització @@ -297,11 +297,11 @@ Protegir el compte S\'haurà d\'admetre els seguidors manualment Guardar l\'esborrany\? - Enviant toot… - Error enviant el toot - Enviant toots + S\'està publicant… + Error en publicar + S\'esatan enviant les publicacions Envio anul·lat - Una copia del toot s\'ha guardat a esborranys + S\'ha guardat una còpia de la publicació als esborranys Escriure La teva instància %s no te emojis personalitzats Estil dels emojis @@ -309,7 +309,7 @@ Hauràs de descarregar el joc d\'emojis Cercant… Expandir/ocultar tots els estats - Obrir toot + Obre la publicació Cal reiniciar l\'aplicació Has de reiniciar l\'aplicació per tal d\'aplicar aquests canvis Més tard @@ -360,7 +360,7 @@ Netejar Filtrar Aplicar - Escriure un toot + Escriure una publicació Escriure Mostra l\'indicador dels bots Vols netejar totes les notificacions permanentment\? @@ -374,8 +374,8 @@ L\'enquesta on has votat està tancada La enquesta que heu creat ha finalitzat Advertència: %s - Toot fixat - Toot no fixat + Fixat + No fixis Fixar Respost Accions per a la imatge %s @@ -385,7 +385,7 @@ Silenciar %s %s visible Amagar el domini sencer - Obrir sempre els toots marcats amb contingut sensible + Mostra sempre obertes les publicacions marcades amb avisos de contingut Paraula sencera Ventall actual d\'emojis de Google Enquesta amb opcions: %1$s, %2$s, %3$s, %4$s; %5$s @@ -418,12 +418,12 @@ Múltiples tries Tria %d Preferits - Toots programats + Publicacions programades Preferit Edita Preferits - Toots programats - Programar el toot + Publicacions programades + Programa la publicació Reiniciar Desenvolupat per Tusky S\'ha afegit a les adreces d\'interès @@ -449,7 +449,7 @@ Silenciar @%s\? Bloquejar @%s\? No silenciar la conversació - Conversació muda + Silencia la conversa %s ha sol·licitat seguir-te A baix A dalt @@ -487,15 +487,15 @@ Adjuncions Àudio Notificacions quan algú a qui esteu subscrit publica un tut nou - Tuts nous + Publicacions noves emojis personalitzats animats algú a qui estic subscrit acaba de publicar un tut nou %s acaba de fer una publicació Avisos - S\'ha esborrat el tut del qual en vau fer un esborrany de resposta + S\'ha eliminat la publicació a la qual vau fer un esborrany de resposta S\'ha eliminat l\'esborrany No s\'ha pogut carregar la informació de la resposta - No s\'ha pogut enviar aquest tut! + No s\'ha pogut publicar! Segur que voleu esborrar la llista %s\? No podeu pujar més de %1$d adjunts multimèdia. From 3d3287cc42dbeb5630c5bc037044fb3ef26abf53 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Mon, 9 May 2022 17:39:49 +0000 Subject: [PATCH 084/104] Translated using Weblate (German) Currently translated at 100.0% (478 of 478 strings) Co-authored-by: Christian Schmidt Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/de/ Translation: Tusky/Tusky --- app/src/main/res/values-de/strings.xml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index b05ccc96..c8d64750 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -25,7 +25,7 @@ Föderiert Direktnachrichten Tabs - Beitrag + Konversation Beiträge mit Antworten Angeheftet @@ -490,7 +490,7 @@ Für immer Anhänge Audio - Benachrichtigungen, wenn jemand, den ich abonniert habe, etwas Neues veröffentlicht + Benachrichtigungen, wenn jemand, den ich abonniert habe, eine neue Nachricht veröffentlicht Neue Beiträge GIF-Emojis animieren Jemand, den ich abonniert habe, hat etwas Neues veröffentlicht @@ -535,4 +535,6 @@ Jemand hat sich registriert Benachrichtigungen, wenn Beiträge bearbeitet werden, mit denen du interagiert hast Anmelden + Die Anmeldeseite konnte nicht geladen werden. + Beitragsbearbeitungen \ No newline at end of file From 23d6d04b399711cdb07a67028f96cd15c6efcedc Mon Sep 17 00:00:00 2001 From: Bruno Miguel Date: Mon, 9 May 2022 17:39:50 +0000 Subject: [PATCH 085/104] Translated using Weblate (Portuguese (Portugal)) Currently translated at 100.0% (478 of 478 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 | 212 +++++++++++---------- 1 file changed, 107 insertions(+), 105 deletions(-) diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index aa81160b..6be06b0c 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -10,20 +10,20 @@ A responder a @%s Editar a lista - Exige a aprovação manual de seguidores + Necessita de aprovar manualmente os seguidores Guardar rascunho\? Depois Desafixar %1$s e %2$s Bem-estar Escrever Toot - Deseja excluir a lista %s\? - Apesar do seu perfil não ser trancado, %1$s exige que você revise a solicitação para te seguir destes perfis manualmente. - Notificar - Cancelar + Pretende remover a lista %s\? + Apesar do seu perfil não ser privado, %1$s exige que você reveja manualmente as solicitações para te seguir destes perfis. + Subscrever + Remover subscrição Autorização negada. Erro ao adquirir token de login. - O toot é muito longo! + 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. @@ -48,13 +48,13 @@ Seguidores Favoritos Itens guardados - Utilizadors silenciados - Utilizadors bloqueados + Utilizadores silenciados + Utilizadores bloqueados Instâncias bloqueadas Seguidores Pendentes Conteúdo sensível Editar perfil - Conteúdo ocultado + Conteúdo multimédia ocultado Rascunhos Toque para ver Mostrar Mais @@ -67,7 +67,7 @@ \@%s %s fez boost Nada aqui. - Nada aqui. Arraste para baixo para atualizar! + Nada para ver aqui. Arraste para baixo para atualizar! %s fez boost ao seu toot %s adicionou o seu toot aos favoritos %s está a seguir-te @@ -77,7 +77,7 @@ %s editou um toot Denunciar @%s Comentários adicionais\? - Resposta rápida + Resposta Rápida Responder Fazer boost Desfazer boost @@ -89,7 +89,7 @@ Escrever Entrar com Mastodon Sair - Tem certeza de que deseja sair da conta %1$s\? + Tem a certeza que deseja sair da conta %1$s\? Seguir Deixar de seguir Bloquear @@ -106,12 +106,12 @@ Tentar novamente Fechar Perfil - Preferências - Preferências da Conta + Configurações + Configurações da Conta Favoritos - Guardados - utilizadors silenciados - utilizadors bloqueados + Itens Guardados + Utilizadores silenciados + Utilizadores bloqueados Instâncias bloqueadas Seguidores Pendentes Conteúdo multimédia @@ -138,13 +138,13 @@ Privacidade do toot Aviso de conteúdo Teclado de emojis - Agendar toot + Agendar Toot Redefinir Adicionar Separador Hiperligações Menções Hashtags - Ver quem fez boost + Ver autor do boost Mostrar boosts Mostrar favoritos Hashtags @@ -152,7 +152,7 @@ Hiperligações Abrir conteúdo multimédia #%d A descarregar %1$s - Copiar hiperligação + Copiar a hiperligação Abrir como %s Partilhar como… Descarregar conteúdo multimédia @@ -178,22 +178,22 @@ Cabeçalho O que é uma instância\? A ligar… - O endereço IP ou domínio de qualquer instância pode ser inserido aqui, como por exemplo mastodon.social, masto.donte.com.br, colorid.es ou qualquer outro! + O endereço IP ou domínio de qualquer instância pode ser inserido aqui, como por exemplo mastodon.social, masto.pt, pleroma.pt ou qualquer outro! \n -\n Se ainda não tem uma conta, insira o nome da instância onde pretende participar e crie uma conta lá. +\nSe ainda não tem uma conta, insira o nome da instância onde pretende participar e crie uma conta lá. \n -\n Uma instância é um lugar onde sua conta é hospedada, mas pode facilmente seguir e comunicar com pessoas de outras instâncias como se todos estivessem no mesmo site. +\nUma instância é o local onde sua conta é criada, mas pode facilmente seguir e comunicar com pessoas de outras instâncias como se estivessem todos no mesmo site. \n \nMais informações disponíveis em joinmastodon.org. - Envio de Conteúdo Multimédia Terminando + A Terminar Envio de Conteúdo Multimédia A enviar… Descarregar - Cancelar pedido para seguir\? + Cancelar o pedido para seguir\? Deixar de seguir esta conta\? Apagar este toot\? - Apagar e criar novo rascunho\? + Apagar e rescrever este toot\? Apagar esta conversa\? - Tem certeza que pretende bloquear a instância %s\? Deixará de poder ver quaisquer conteúdos dessa instância em qualquer timeline pública ou nas suas notificações. Os seus seguidores dessa instância serão removidos. + Tem a certeza que pretende bloquear a instância %s\? Deixará de poder ver quaisquer conteúdos dessa instância em qualquer timeline pública ou nas suas notificações. Os seus seguidores dessa instância serão removidos. Bloquear instância Bloquear @%s\? Silenciar @%s\? @@ -202,7 +202,7 @@ Não listado: Não publicar em timelines públicas Privado: Publicar apenas para os seguidores Direto: Publicar apenas para os utilizadores mencionados - Editar notificações + Notificações Notificações Alertas Notificar com som @@ -213,17 +213,17 @@ for seguido alguém para quem ativei os alertas publicar um toot novo fizerem pedido para me seguir - derem boosts nos meus toots + fizerem boosts aos meus toots adicionarem os meus toots aos favoritos votações terminarem alguém criar conta um toot com o qual interagi for editado Aparência - Temas + Tema da Aplicação Timelines Filtros - Noturno - Diurno + Escuro + Claro AMOLED Automático ao pôr-do-sol Usar o Tema do Sistema @@ -232,11 +232,11 @@ Esconder o botão de criação de toots ao fazer scroll Idioma Mostrar indicador para bots - Reproduzir avatares em GIFs + Reproduzir avatars em GIF Mostrar desfocagem em conteúdo multimédia sensível Animar emojis personalizados Filtro da timeline - Separadors + Separadores Mostrar boosts Mostrar respostas Mostrar pré-visualização de conteúdo multimédia @@ -268,23 +268,23 @@ Notificações para seguidores pendentes Boosts Votações - Notificações para votações que terminaram - Notificações quando alguém para quem ativei os alertas publicar um toot novo + Notificações para votações terminadas + Notificações quando alguém para quem ativou os alertas publicar um toot novo Notificações para novos utilizadores - Edições a toots + Edições de toots Notificações para boosts recebidos Favoritos - Notificações quando os teus toots são adicionados aos favoritos - Notificações quando toots com os quais interagi foram editados + Notificações quando os seus toots são adicionados aos favoritos + Notificações quando toots com os quais interagiu forem editados %s mencionou-te %1$s, %2$s, %3$s e %4$d outros %1$s, %2$s e %3$s - Perfil Bloqueado + Perfil Privado Sobre Tusky %s A correr o Tusky Atualizar - Tusky é um software livre e de código aberto e é ljcenciado com a versão 3 da GNU General Public License. Leia a licença aqui: https://www.gnu.org/licenses/gpl-3.0.pt-br.html + Tusky é um software livre e de código aberto, licenciado com a versão 3 da GNU General Public License. Pode ler a licença aqui: https://www.gnu.org/licenses/gpl-3.0.pt-br.html Página do projeto: \n https://tusky.app Reporte de erros e pedidos de funcionalidades: @@ -296,13 +296,13 @@ Vídeo Áudio Anexos - Pedido enviado + Pedido para seguir enviado em %dy em %dd em %dh em %dm em %ds - %da + %dy %dd %dh %dm @@ -325,7 +325,7 @@ Listas Não foi possível renomear a lista Listas - Lista da timeline + Cronologia da timeline Não foi possível criar a lista Não foi possível apagar a lista Criar uma lista @@ -338,10 +338,11 @@ Erro ao incluir descrição Descrição para deficientes visuais +\n(até %d letra) + Descrição para deficientes visuais \n(até %d caracteres) - - Descrever + Escrever descrição Remover Bloquear perfil A enviar o toot… @@ -351,34 +352,34 @@ Uma cópia do toot foi guardada nos seus rascunhos Escrever A sua instância, %s, não tem emojis personalizados - Estilo de emoji + Estilo dos emojis Padrão do sistema É necessário descarregar estes pacotes de emojis primeiro A fazer pesquisa… Expandir/Contrair todos os toots Abrir toot É necessário reiniciar a aplicação - É necessário reiniciar o aplicativo para aplicar as alterações + É necessário reiniciar o Tusky para aplicar as alterações Reiniciar Pacote de emojis padrão do seu dispositivo - Emojis padrão do Android da versão 4.4 até 7.1 + Emojis padrão do Android 4.4 até ao 7.1 Pacote de emojis padrão do Mastodon - Pacote de emojis atual do Google - Erro ao baixar + Pacote de emojis atuais da Google + Erro ao descarregar Robô %1$s mudou-se para: - Dar boost para o mesmo público + Dar boost para o público inicial Desfazer boost O Tusky contém código e recursos dos seguintes projetos de código aberto: - Licenciado sob a licença Apache (cópia separadorixo) + Licenciado sob a licença Apache (cópia abaixo) CC-BY 4.0 CC-BY-SA 4.0 Metadados do perfil - Adicionar + adicionar dados Rótulo Conteúdo - Usar tempo absoluto - As informações separadorixo podem refletir incompletamente o perfil do utilizador. Toque aqui para abrir o perfil completo no navegador. + Usar data absoluta + As informações abaixo podem refletir, de forma incompleta, o perfil do utilizador. Toque aqui para abrir o perfil completo no navegador. Fixar %1$s Favorito @@ -388,25 +389,25 @@ %s Boost %s Boosts - Levou boost de - Favoritado por + Boost dado por + Adicionado aos favoritos por %1$s - %1$s, %2$s e %3$d outros + %1$s, %2$s e %3$d mais - excedeu o máximo de %1$d separador - excedeu o máximo de %1$d separadors + atingiu o máximo de %1$d separador + atingiu o máximo de %1$d separadores - conteúdo multimédia: %s + Conteúdo multimédia: %s Aviso de Conteúdo: %s Sem descrição - Você fez boost - Favoritado - Salvo + Replicado + Adicionado aos favoritos + Guardado Público Não-listado Privado Direto - Enquete com as opções: %1$s, %2$s, %3$s, %4$s; %5$s + Votação com as opções: %1$s, %2$s, %3$s, %4$s; %5$s Nome da lista Adicionar hashtag Hashtag sem # @@ -414,11 +415,11 @@ Selecionar lista Lista Limpar - Filtro - Salvar - Compor toot - Compor - Tem certeza de que deseja limpar permanentemente todas as suas notificações\? + Filtrar + Aplicar + Escrever toot + Escrever + Tem certeza que pretende limpar permanentemente todas as suas notificações\? Opções para imagem %s %1$s • %2$s @@ -430,10 +431,10 @@ %s pessoas termina em %s - Terminou + terminada Votar - Uma enquete que você votou terminou - Sua enquete terminou + Uma votação em que votou terminou + A sua votação terminou %d dia restante %d dias restantes @@ -451,20 +452,20 @@ %d segundos restantes Continuar - Voltar - Ok + Retroceder + Feito \@%s denunciado com sucesso Comentários adicionais Encaminhar para %s Erro ao denunciar Erro ao carregar toots - A denúncia será enviada aos moderadores da instância. Explique por que denunciou a conta: - A conta está em outra instância. Enviar uma cópia anônima da denúncia para lá\? + A denúncia será enviada aos moderadores da instância. Pode adicionar abaixo uma explicação para a sua denúncia: + A conta está noutra instância. Quer enviar uma cópia anónima da denúncia para lá\? Contas Erro ao pesquisar - Mostrar filtro de notificações - Ativar deslizar para alternar entre separadors - Enquete + Mostrar Filtro das Notificações + Ativar gesto de deslizar para alternar entre separadores + Votação Duração Indefinido 5 minutos @@ -481,46 +482,46 @@ 180 dias 365 dias Adicionar opção - Múltiplas opções + Escolha múltipla Opção %d Editar - Erro ao pesquisar %s - Sem rascunhos. - Sem toots agendados. - Salvo! + Erro ao pesquisar toot %s + Não tem rascunhos. + Não tem toots agendados. + Guardado! Algumas informações que podem afetar seu bem-estar serão ocultadas. Isso inclui: \n \n- Notificações de favoritos, boosts e seguidores \n- Número de favoritos e boosts nos toots \n- Status de toots e seguidores nos perfis \n -\nNotificações push não serão afetadas, mas é possível revisar sua preferência manualmente. - Revisar notificações +\nNotificações push não serão afetadas, mas é possível rever as configurações das notificações manualmente. + Rever Notificações Limitar notificações da timeline - Sem comunicados. - Mastodon possui um intervalo mínimo de 5 minutos para agendar. - Mostrar prévias de Hiperligações nas linhas - Solicitar confirmação antes de dar boost - Solicitar confirmação antes de favoritar - Esconder o título da barra superior de tarefas - Nota pessoal sobre este perfil aqui - Esconder status dos toots - Esconder status dos perfis + Sem anúncios. + O Mastodon tem um intervalo mínimo de agendamento de 5 minutos. + Mostrar pré-visualização de hiperligações nas timelines + Mostrar janela de confirmação antes de dar boost + Mostrar janela de confirmação antes de adicionar aos favoritos + Esconder o título da barra superior + Nota pessoal sobre este perfil + Esconder estatísticas quantitativas nos toots + Esconder estatísticas quantitativas nos perfis - Não é possível anexar mais de %1$d arquivo de conteúdo multimédia. - Não é possível anexar mais de %1$d arquivos de conteúdo multimédia. + Não é possível enviar mais de %1$d arquivo de conteúdo multimédia. + Não é possível enviar mais de %1$d arquivos de conteúdo multimédia. Erro ao enviar o toot! - Erro ao carregar toot para responder - Rascunho excluído - O toot em que se rascunhou uma resposta foi excluído + Erro ao carregar informação de resposta + Rascunho apagado + O toot para o qual escreveu um rascunho foi apagado Ocorreu um erro. Ocorreu um erro de conetividade! Por favor, verifique a sua ligação e tente novamente! Isto não pode estar vazio. - Instância inválida inserida + A instância inserida é inválida Erro ao autenticar com esta instância. - Nao foi possível encontrar um navegador. - Ocorreu um erro não identificado de autorização. + Não foi possível encontrar um navegador. + Ocorreu um erro de autorização não identificado. Entrar Guardar Editar perfil @@ -528,4 +529,5 @@ Desfazer Aceitar Rejeitar + Não foi possível carregar a página de login \ No newline at end of file From b353e67587355fc1dc07eb1db30a2643b2358684 Mon Sep 17 00:00:00 2001 From: Christian Schmidt Date: Mon, 9 May 2022 11:40:35 +0000 Subject: [PATCH 086/104] Translated using Weblate (German) Currently translated at 100.0% (16 of 16 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/de/ --- fastlane/metadata/android/de/changelogs/89.txt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 fastlane/metadata/android/de/changelogs/89.txt diff --git a/fastlane/metadata/android/de/changelogs/89.txt b/fastlane/metadata/android/de/changelogs/89.txt new file mode 100644 index 00000000..cb92453b --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Öffnen als..." ist jetzt im Menü in Konto Profilen auch verfügbar, wenn mehrere Konten genutzt werden +- Die Anmeldung wird jetzt über die WebView innerhalb der App abgewickelt +- Unterstützung für Android 12 +- Unterstützung für die neue Mastodon instance configuration API +- und einige andere kleine Fehlerbehebungen und Verbesserungen From 7db504775c094c333f8e012ef797d4ece06f2d88 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Tue, 10 May 2022 20:00:53 +0200 Subject: [PATCH 087/104] Release 90 --- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/91.txt | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/91.txt diff --git a/app/build.gradle b/app/build.gradle index 4484ff54..c152805b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,8 +20,8 @@ android { applicationId APP_ID minSdkVersion 21 targetSdkVersion 31 - versionCode 89 - versionName "17.0" + versionCode 90 + versionName "18.0 beta 1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/fastlane/metadata/android/en-US/changelogs/91.txt b/fastlane/metadata/android/en-US/changelogs/91.txt new file mode 100644 index 00000000..e1d98303 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Support for new Mastodon 3.5 notification types +- The bot badge now looks better and adjusts to the selected theme +- Text can now be selected on the post detail view +- Fixed a lot of bugs, including one that prevented logins on Android 6 and lower From 0f1e95d0ca81d0254f8d1ae6b60a3130d51b15cf Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Wed, 11 May 2022 07:43:30 +0200 Subject: [PATCH 088/104] add 35.json --- .../35.json | 821 ++++++++++++++++++ 1 file changed, 821 insertions(+) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/35.json diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/35.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/35.json new file mode 100644 index 00000000..9b71adf2 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/35.json @@ -0,0 +1,821 @@ +{ + "formatVersion": 1, + "database": { + "version": 35, + "identityHash": "9e6c0bb60538683a16c30fa3e1cc24f2", + "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, `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)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "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 + } + ], + "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, 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 + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT 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": "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, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_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": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.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, '9e6c0bb60538683a16c30fa3e1cc24f2')" + ] + } +} \ No newline at end of file From 4159826f266d7c6b69738d0668c43b33b68b51c8 Mon Sep 17 00:00:00 2001 From: mcclure Date: Wed, 11 May 2022 11:16:51 -0400 Subject: [PATCH 089/104] Allow build on systems without git (#2514) Set git revision to "unknown" if git not available. --- app/build.gradle | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c152805b..2a34c80a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,9 +7,13 @@ apply from: "../instance-build.gradle" def getGitSha = { def stdout = new ByteArrayOutputStream() - exec { - commandLine 'git', 'rev-parse', '--short', 'HEAD' - standardOutput = stdout + try { + exec { + commandLine 'git', 'rev-parse', '--short', 'HEAD' + standardOutput = stdout + } + } catch (Exception e) { + return "unknown" } return stdout.toString().trim() } From b8e3b6b884d0fed3005f21fc52da3816208e2894 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 12 May 2022 18:21:33 +0200 Subject: [PATCH 090/104] fix currently logged in profiles not being visible in main drawer when offline (#2516) --- app/src/main/java/com/keylesspalace/tusky/MainActivity.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 466dba16..25b70240 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -621,6 +621,8 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje binding.mainToolbar.setOnClickListener { (adapter.getFragment(activeTabLayout.selectedTabPosition) as? ReselectableFragment)?.onReselect() } + + updateProfiles() } private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean { From d9c6269d4448e40a7a07ba6d209988b2b5eabe2c Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 12 May 2022 18:21:43 +0200 Subject: [PATCH 091/104] fix deleting media attachments removing the wrong ones (#2517) --- .../keylesspalace/tusky/components/compose/ComposeViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 abf2ff42..7faf1139 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 @@ -184,7 +184,7 @@ class ComposeViewModel @Inject constructor( fun removeMediaFromQueue(item: QueuedMedia) { mediaToJob[item.localId]?.cancel() - media.update { mediaValue -> mediaValue.filter { it.localId == item.localId } } + media.update { mediaValue -> mediaValue.filter { it.localId != item.localId } } } fun toggleMarkSensitive() { From 0a0f31451660f388edfa9ac9373a75549d6f8490 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Thu, 12 May 2022 01:40:36 +0000 Subject: [PATCH 092/104] Translated using Weblate (Ukrainian) Currently translated at 100.0% (17 of 17 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/uk/ --- fastlane/metadata/android/uk/changelogs/91.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 fastlane/metadata/android/uk/changelogs/91.txt diff --git a/fastlane/metadata/android/uk/changelogs/91.txt b/fastlane/metadata/android/uk/changelogs/91.txt new file mode 100644 index 00000000..4132d155 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Підтримка нових типів сповіщень Mastodon 3.5 +- Кращий вигляд позначки бота і розширений вибір тем +- Текст тепер можна вибрати у докладному поданні допису +- Виправлено безліч помилок, включно з тою, яка перешкоджала входу на Android 6 і старіших From 523c9b6b8ccc38a418a146229c39cf52abf10c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Thu, 12 May 2022 01:40:36 +0000 Subject: [PATCH 093/104] Translated using Weblate (Vietnamese) Currently translated at 100.0% (17 of 17 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/vi/ --- fastlane/metadata/android/vi/changelogs/91.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 fastlane/metadata/android/vi/changelogs/91.txt diff --git a/fastlane/metadata/android/vi/changelogs/91.txt b/fastlane/metadata/android/vi/changelogs/91.txt new file mode 100644 index 00000000..2835fdfc --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Hỗ trợ những kiểu thông báo mới của Mastodon 3.5 +- Nhãn của tài khoản nhìn đẹp hơn và thay đổi theo chủ đề +- Cho phép chọn và sao chép nội dung tút +- Sửa lỗi chặn đăng nhập trên Android 6 trở xuống From 354b07aa737fba8c39840ed31ec4bf81e939a9e6 Mon Sep 17 00:00:00 2001 From: Agee Kalisz Date: Fri, 13 May 2022 18:40:36 +0000 Subject: [PATCH 094/104] Translated using Weblate (Polish) Currently translated at 100.0% (479 of 479 strings) Co-authored-by: Agee Kalisz Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/pl/ Translation: Tusky/Tusky --- app/src/main/res/values-pl/strings.xml | 50 +++++++++++++------------- 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index a1424b3a..3fce8b6a 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -20,7 +20,7 @@ Strona główna Powiadomienia Lokalne - Globalne + Sfederowane Wątek Wpisy Z odpowiedziami @@ -33,12 +33,12 @@ Edytuj profil Szkice Licencje - %s podbił - Wrażliwe treści - Ukryto zawartość multimedialną + %s podbite + Treści wrażliwe + Ukryto multimedia Naciśnij, aby wyświetlić Pokaż więcej - Ukryj + Pokaż mniej Pusto tutaj. Pociągnij, aby odświeżyć! %s podbił(-a) Twój wpis %s dodał Twój post do ulubionych @@ -95,13 +95,13 @@ Klawiatura emoji Pobieranie %1$s Skopiuj odnośnik - Udostępnij odnośnik do wpisu… + Udostępnij URL do… Udostępnij wpis do… Wyślij! Odblokowano użytkownika Cofnięto wyciszenie użytkownika Wyślij! - Pomyślnie wysłano odpowiedź. + Odpowiedź wysłano pomyślnie. Jaka instancja? Co Ci chodzi po głowie? Ostrzeżenie o zawartości @@ -151,7 +151,7 @@ Używaj niestandardowych kart Chrome Ukryj przycisk śledzenia podczas przewijania Filtrowanie osi czasu - Zakładki + Karty Pokaż podbicia Pokazuj odpowiedzi Pokazuj podgląd zawartości multimedialnej @@ -183,10 +183,10 @@ %1$s, %2$s, i %3$s %1$s i %2$s - %d nowe powiadomienie - %d nowe powiadomienia - %d nowych powiadomień - %d nowych powiadomień + %d nowa interakcja + %d nowe interakcje + %d nowych interakcji + %d nowych interakcji Konto zablokowane O programie @@ -404,25 +404,25 @@ Głosowanie w którym brałeś(-aś) udział zakończyła się Ankieta, którą stworzyłeś(aś), zakończyła się - Zostało %d dzień + Został %d dzień Zostało %d dni Zostało %d dni Zostało %d dni - Zostało %d godzina + Została %d godzina Zostało %d godziny Zostało %d godzin Zostało %d godzin - Zostało %d minuta + Została %d minuta Zostało %d minuty Zostało %d minut Zostało %d minut - Zostało %d sekunda + Została %d sekunda Zostało %d sekund Zostało %d sekund Zostało %d sekund @@ -462,7 +462,7 @@ Zakładki Dodaj do zakładek Zakładki - Dodane do zakładek + Dodany do zakładek Wybierz listę Lista Pliki audio muszą być mniejsze niż 40MB. @@ -493,8 +493,8 @@ Dół Góra - Nie możesz przesłać więcej niż %1$d załącznika. - Nie możesz przesłać więcej niż %1$d załączników. + Nie możesz przesłać więcej niż %1$d załącznik. + Nie możesz przesłać więcej niż %1$d załączniki. Nie możesz przesłać więcej niż %1$d załączników. Nie możesz przesłać więcej niż %1$d załączników. @@ -515,21 +515,21 @@ Włącz gest przesuwania by przełączać między zakładkami Załączniki Powiadomienia o prośbach o obserwowanie - ktoś kogo zasubskrybowałem/zasubskrybowałam opublikował nowy wpis + ktoś zasubskrybowany opublikował nowy wpis Wysłano prośbę o obserwowanie Ogłoszenia Zdrowie Anuluj subskrypcję Zasubskrybuj Mimo tego, że twoje konto nie jest zablokowane, administracja %1$s uznała, że możesz chcieć ręcznie przejrzeć te prośby o możliwość śledzenia od tych kont. - Wpis dla którego naszkicowałeś/naszkicowałaś odpowiedź został usunięty + Wpis dla którego naszkicowałeś/aś odpowiedź został usunięty Usunięto szkic Ukryj ilościowe statystyki na profilach Ukryj ilościowe statystyki na postach Przejrzyj powiadomienia Zapisano! Twoja prywatna notatka o tym koncie - Nieskończona + Nieograniczony Dźwięk Powiadomienia o opublikowaniu nowego wpisu przez kogoś, kogo obserwujesz Pozycja głównego paska nawigacji @@ -552,9 +552,11 @@ %s zarejestrował(a) się Rejestracje Powiadomienia o nowych użytkownikach - Powiadomienia o edycji wpisów z którymi interaktowałeś/aś + Powiadomienia o edycji wpisów z którymi dokonałeś/aś interakcji ktoś zarejestrował się - wpis, z którym interaktowałem/am został edytowany + wpis, z którym dokonałem/am interakcji został edytowany %s edytował(a) swój wpis Edycje wpisów + Zapisywanie szkicu… + Nie można załadować strony logowania. \ No newline at end of file From 6f515ad98a26a148ee2929ada665dbbf9e1b00e2 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 13 May 2022 18:40:36 +0000 Subject: [PATCH 095/104] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (479 of 479 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 f3d7b2aa..862cf06c 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -536,4 +536,5 @@ 嘟文编辑 当你进行过互动的嘟文被编辑时发出通知 无法加载登录页。 + 正在保存草稿… \ No newline at end of file From 47eabafed38f5435c9238bcabd8fbd5a0c5b259e Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Fri, 13 May 2022 18:40:36 +0000 Subject: [PATCH 096/104] Translated using Weblate (Ukrainian) Currently translated at 100.0% (479 of 479 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 | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 6c510097..5998c9b0 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -550,4 +550,5 @@ Редакції допису Вхід Не вдалося завантажити сторінку входу. + Збереження чернетки… \ No newline at end of file From a6dc7ef425872bfe0fd4a3c14f996d2c829b8ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Fri, 13 May 2022 18:40:36 +0000 Subject: [PATCH 097/104] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (479 of 479 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 0c8109db..bdf3af84 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -517,4 +517,5 @@ Thông báo khi tút mà tôi tương tác bị sửa Đăng nhập Không thể tải trang đăng nhập. + Đang lưu nháp… \ No newline at end of file From 8fc2c1601eaa74c1b98eaacd8fcca32a266f7299 Mon Sep 17 00:00:00 2001 From: GunChleoc Date: Fri, 13 May 2022 18:40:37 +0000 Subject: [PATCH 098/104] Translated using Weblate (Gaelic) Currently translated at 100.0% (479 of 479 strings) Translated using Weblate (Gaelic) Currently translated at 100.0% (479 of 479 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 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index e27b677a..d01b0dca 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -295,7 +295,7 @@ Cuir post air an sgeideal Faicsinneachd a’ phuist Postaichean air an sgeideal - Chuir %s am post agad ris na h-annsachdan + Is annsa le %s am post agad Bhrosnaich %s am post agad Postaichean air an sgeideal Snàithlean @@ -556,4 +556,5 @@ chaidh post a rinn mi conaltradh leis a deasachadh Clàraich a-steach Cha b’ urrainn dhuinn duilleag a’ chlàraidh a-steach fhosgladh. + A’ sàbhaladh na dreuchd… \ No newline at end of file From 8c6ccf426103b4ceac93b14d981f1f009f980225 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 13 May 2022 22:00:30 +0200 Subject: [PATCH 099/104] fix notification message formatting when username is not at the beginning of the message (#2522) * fix notification message formatting when username is not at the beginning of the message * search for placeholder in format string --- .../tusky/adapter/NotificationsAdapter.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index f681e64f..327e4aa7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -531,8 +531,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter { message.setCompoundDrawablesWithIntrinsicBounds(icon, null, null, null); String wholeMessage = String.format(format, displayName); final SpannableStringBuilder str = new SpannableStringBuilder(wholeMessage); - str.setSpan(new StyleSpan(Typeface.BOLD), 0, displayName.length(), - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + int displayNameIndex = format.indexOf("%s"); + str.setSpan( + new StyleSpan(Typeface.BOLD), + displayNameIndex, + displayNameIndex + displayName.length(), + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ); CharSequence emojifiedText = CustomEmojiHelper.emojify( str, notificationViewData.getAccount().getEmojis(), message, statusDisplayOptions.animateEmojis() ); From ec72cd0b52716e297ef256a75205f9129e642256 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Tue, 17 May 2022 09:40:38 +0000 Subject: [PATCH 100/104] Translated using Weblate (French) Currently translated at 99.5% (477 of 479 strings) Co-authored-by: ButterflyOfFire Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fr/ Translation: Tusky/Tusky --- app/src/main/res/values-fr/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index dc30483b..d6b6e44f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -295,7 +295,7 @@ Mettre une légende Supprimer le média Verrouiller le compte - Vous devez approuvez manuellement les abonnements + Vous devez approuver manuellement les abonnements Enregistrer comme brouillon ? Envoi du pouet… Erreur lors de l’envoi du pouet From 725ce02ab1feb041cca51a62330c7b2d1e1a7416 Mon Sep 17 00:00:00 2001 From: hebbeff Date: Tue, 17 May 2022 09:40:38 +0000 Subject: [PATCH 101/104] Translated using Weblate (Chinese (Traditional)) Currently translated at 91.6% (439 of 479 strings) Co-authored-by: hebbeff Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hant/ Translation: Tusky/Tusky --- app/src/main/res/values-zh-rTW/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 9fd135f4..02d7d2ed 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -525,4 +525,6 @@ 總是顯示被標注為內容警告的嘟文 搜尋失敗 帳號 + 登入 + 無法載入登入頁面。 \ No newline at end of file From 0bf71e642017382347042c1f84a764cc7b243b76 Mon Sep 17 00:00:00 2001 From: GunChleoc Date: Tue, 17 May 2022 09:40:38 +0000 Subject: [PATCH 102/104] Translated using Weblate (Gaelic) Currently translated at 100.0% (479 of 479 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, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index d01b0dca..317331dc 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -92,7 +92,7 @@ Brathan nuair a dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh post ùr Postaichean ùra dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh post ùr - Tha %s air rud a phostadh + Phostaich %s rud Chan eil brath-fios ann. Brathan-fios Chaidh a shàbhaladh! From 74e139c11014772249fb7b85a24016555c3d2811 Mon Sep 17 00:00:00 2001 From: hebbeff Date: Tue, 17 May 2022 09:40:38 +0000 Subject: [PATCH 103/104] Translated using Weblate (Chinese (Simplified)) Currently translated at 88.2% (15 of 17 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/zh_Hans/ --- fastlane/metadata/android/zh-Hans/changelogs/83.txt | 3 +++ fastlane/metadata/android/zh-Hans/changelogs/87.txt | 8 ++++++++ 2 files changed, 11 insertions(+) create mode 100644 fastlane/metadata/android/zh-Hans/changelogs/83.txt create mode 100644 fastlane/metadata/android/zh-Hans/changelogs/87.txt diff --git a/fastlane/metadata/android/zh-Hans/changelogs/83.txt b/fastlane/metadata/android/zh-Hans/changelogs/83.txt new file mode 100644 index 00000000..e8f7c36e --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +此版本修复了给图片添加标题时会崩溃的问题 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/87.txt b/fastlane/metadata/android/zh-Hans/changelogs/87.txt new file mode 100644 index 00000000..06fcd290 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- 时间线加载逻辑完全重写,提升了流畅度、稳定性,更便于维护。 +- APNG和动画WebP格式的动态自定义表情符号。 +- 修正大量BUG +- 支持Android 11 +- 新增界面语言支持:苏格兰盖尔语、加利西亚语、乌克兰语 +- 改进翻译 From 20f3ec921f4040b1a1ca69ee89aa9f27b3561c2f Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Tue, 17 May 2022 19:24:17 +0200 Subject: [PATCH 104/104] Release 91 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2a34c80a..34e941ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,8 +24,8 @@ android { applicationId APP_ID minSdkVersion 21 targetSdkVersion 31 - versionCode 90 - versionName "18.0 beta 1" + versionCode 91 + versionName "18.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true