From 4188670b422413d4b926bffb8021651f92a63fe3 Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Fri, 20 May 2022 16:47:45 +0200 Subject: [PATCH 01/82] Implement reply count indicator to track web UI (#2467) Addresses #882 --- .../37.json | 869 ++++++++++++++++++ .../tusky/adapter/StatusBaseViewHolder.java | 11 +- .../conversation/ConversationEntity.kt | 3 + .../conversation/ConversationViewData.kt | 1 + .../timeline/TimelineTypeMappers.kt | 7 +- .../keylesspalace/tusky/db/AppDatabase.java | 10 +- .../com/keylesspalace/tusky/db/TimelineDao.kt | 2 +- .../tusky/db/TimelineStatusEntity.kt | 1 + .../com/keylesspalace/tusky/di/AppModule.kt | 2 +- .../com/keylesspalace/tusky/entity/Status.kt | 1 + app/src/main/res/layout/item_status.xml | 10 + app/src/main/res/values/strings.xml | 1 + .../tusky/BottomSheetActivityTest.kt | 1 + .../com/keylesspalace/tusky/FilterTest.kt | 1 + .../tusky/components/timeline/StatusMocker.kt | 1 + .../keylesspalace/tusky/db/TimelineDaoTest.kt | 1 + 16 files changed, 917 insertions(+), 5 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/37.json diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/37.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/37.json new file mode 100644 index 000000000..8d748248c --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/37.json @@ -0,0 +1,869 @@ +{ + "formatVersion": 1, + "database": { + "version": 37, + "identityHash": "11033751d382aa8a1c6fc68833097d35", + "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, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, 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, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '11033751d382aa8a1c6fc68833097d35')" + ] + } +} \ No newline at end of file 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 2a5b3f2c6..8b7d6eecd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -71,10 +71,10 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { public static class Key { public static final String KEY_CREATED = "created"; } - private TextView displayName; private TextView username; private ImageButton replyButton; + private TextView replyCountLabel; private SparkButton reblogButton; private SparkButton favouriteButton; private SparkButton bookmarkButton; @@ -123,6 +123,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { content = itemView.findViewById(R.id.status_content); avatar = itemView.findViewById(R.id.status_avatar); replyButton = itemView.findViewById(R.id.status_reply); + replyCountLabel = itemView.findViewById(R.id.status_replies); reblogButton = itemView.findViewById(R.id.status_inset); favouriteButton = itemView.findViewById(R.id.status_favourite); bookmarkButton = itemView.findViewById(R.id.status_bookmark); @@ -360,6 +361,13 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { } + private void setReplyCount(int repliesCount) { + // This label only exists in the non-detailed view (to match the web ui) + if (replyCountLabel != null) { + replyCountLabel.setText((repliesCount > 1 ? replyCountLabel.getContext().getString(R.string.status_count_one_plus) : Integer.toString(repliesCount))); + } + } + private void setReblogged(boolean reblogged) { reblogButton.setChecked(reblogged); } @@ -733,6 +741,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { setUsername(status.getUsername()); setCreatedAt(actionable.getCreatedAt(), statusDisplayOptions); setIsReply(actionable.getInReplyToId() != null); + setReplyCount(actionable.getRepliesCount()); setAvatar(actionable.getAccount().getAvatar(), status.getRebloggedAvatar(), actionable.getAccount().getBot(), statusDisplayOptions); setReblogged(actionable.getReblogged()); 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 f585b4ea5..5462ea7b5 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 @@ -79,6 +79,7 @@ data class ConversationStatusEntity( val createdAt: Date, val emojis: List, val favouritesCount: Int, + val repliesCount: Int, val favourited: Boolean, val bookmarked: Boolean, val sensitive: Boolean, @@ -107,6 +108,7 @@ data class ConversationStatusEntity( emojis = emojis, reblogsCount = 0, favouritesCount = favouritesCount, + repliesCount = repliesCount, reblogged = false, favourited = favourited, bookmarked = bookmarked, @@ -149,6 +151,7 @@ fun Status.toEntity() = createdAt = createdAt, emojis = emojis, favouritesCount = favouritesCount, + repliesCount = repliesCount, favourited = favourited, bookmarked = bookmarked, sensitive = sensitive, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt index 470675d17..d63fce6c1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt @@ -71,6 +71,7 @@ fun StatusViewData.Concrete.toConversationStatusEntity( createdAt = status.createdAt, emojis = status.emojis, favouritesCount = status.favouritesCount, + repliesCount = status.repliesCount, favourited = favourited, bookmarked = bookmarked, sensitive = status.sensitive, 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 12422a954..8b96283fd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt @@ -99,6 +99,7 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity { contentShowing = false, pinned = false, card = null, + repliesCount = 0 ) } @@ -140,6 +141,7 @@ fun Status.toEntity( contentCollapsed = contentCollapsed, pinned = actionableStatus.pinned == true, card = actionableStatus.card?.let(gson::toJson), + repliesCount = actionableStatus.repliesCount ) } @@ -183,6 +185,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { muted = status.muted, poll = poll, card = card, + repliesCount = status.repliesCount ) } val status = if (reblog != null) { @@ -211,7 +214,8 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { pinned = status.pinned, muted = status.muted, poll = null, - card = null + card = null, + repliesCount = status.repliesCount, ) } else { Status( @@ -240,6 +244,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { muted = status.muted, poll = poll, card = card, + repliesCount = status.repliesCount, ) } 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 25fe1a61f..c028c88a8 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 = 36) + }, version = 37) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -553,4 +553,12 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `pushServerKey` TEXT NOT NULL DEFAULT ''"); } }; + + public static final Migration MIGRATION_36_37 = new Migration(36, 37) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `repliesCount` INTEGER NOT NULL DEFAULT 0"); + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_repliesCount` INTEGER NOT NULL DEFAULT 0"); + } + }; } 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 2c6ef1887..123ebe211 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt @@ -34,7 +34,7 @@ abstract class TimelineDao { """ 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.emojis, s.reblogsCount, s.favouritesCount, s.repliesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive, s.spoilerText, s.visibility, s.mentions, s.tags, s.application, s.reblogServerId,s.reblogAccountId, s.content, s.attachments, s.poll, s.card, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned, a.serverId as 'a_serverId', a.timelineUserId as 'a_timelineUserId', 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 2c4d45c32..ecd3c0ce5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt @@ -61,6 +61,7 @@ data class TimelineStatusEntity( val emojis: String?, val reblogsCount: Int, val favouritesCount: Int, + val repliesCount: Int, val reblogged: Boolean, val bookmarked: Boolean, val favourited: Boolean, 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 85741762d..08069ae88 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -64,7 +64,7 @@ class AppModule { 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_34_35, - AppDatabase.MIGRATION_35_36, + AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, ) .build() } 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 19cb7aa64..72a37f913 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -34,6 +34,7 @@ data class Status( val emojis: List, @SerializedName("reblogs_count") val reblogsCount: Int, @SerializedName("favourites_count") val favouritesCount: Int, + @SerializedName("replies_count") val repliesCount: Int, var reblogged: Boolean, var favourited: Boolean, var bookmarked: Boolean, diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml index af4bca168..027e10d37 100644 --- a/app/src/main/res/layout/item_status.xml +++ b/app/src/main/res/layout/item_status.xml @@ -319,6 +319,16 @@ app:layout_constraintTop_toBottomOf="@id/status_poll_description" app:srcCompat="@drawable/ic_reply_24dp" /> + + Video Audio Attachments + 1+ Follow requested diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt index beb6af9b4..503a03176 100644 --- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt @@ -74,6 +74,7 @@ class BottomSheetActivityTest { emojis = emptyList(), reblogsCount = 0, favouritesCount = 0, + repliesCount = 0, reblogged = false, favourited = false, bookmarked = false, diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt index 91ea38d3b..521f01d69 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt @@ -166,6 +166,7 @@ class FilterTest { emojis = emptyList(), reblogsCount = 0, favouritesCount = 0, + repliesCount = 0, reblogged = false, favourited = false, bookmarked = false, 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 cc6a90bd9..8781f6d9e 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 @@ -29,6 +29,7 @@ fun mockStatus(id: String = "100") = Status( emojis = emptyList(), reblogsCount = 1, favouritesCount = 2, + repliesCount = 3, reblogged = false, favourited = true, bookmarked = true, 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 ed652418a..620f73403 100644 --- a/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/db/TimelineDaoTest.kt @@ -443,6 +443,7 @@ class TimelineDaoTest { emojis = "emojis$statusId", reblogsCount = 1 * statusId.toInt(), favouritesCount = 2 * statusId.toInt(), + repliesCount = 3 * statusId.toInt(), reblogged = even, favourited = !even, bookmarked = false, From 00c139190e1ff8be6ea14f139e9f1ab4911b8145 Mon Sep 17 00:00:00 2001 From: mcclure Date: Sun, 22 May 2022 15:01:14 -0400 Subject: [PATCH 02/82] Ability to crop images attached to posts (#2531) * First attachment crop attempt: Can crop in place, but does not delete/replace on server so has no effect * Attachment crop feature works * ktlint fixes on attachment crop patch * Upgrade Android-Image-Cropper to 4.2.1 * An error message should be displayed if attachment cropping fails and it is not because the user intentionally cancelled. * Remove 2 of the 3 "state passing" variables by using MediaUtils * Cropper should use content uri (MIME type bearing) and setOutputCompressFormat so that PNGs reach the server safely. * Change to crop requested by Conny: Store inflight cropImageItemOld in view model * Change to crop requested by Conny: Sort cropImage with the other contracts * ktlint fixes on attachment crop patch (again) --- app/build.gradle | 2 +- .../components/compose/ComposeActivity.kt | 53 +++++++++++++++++++ .../components/compose/ComposeViewModel.kt | 27 ++++++++-- .../components/compose/MediaPreviewAdapter.kt | 7 ++- .../tusky/components/compose/MediaUploader.kt | 4 +- app/src/main/res/values/strings.xml | 2 + 6 files changed, 86 insertions(+), 9 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 0b41eb704..821a8e5cb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -175,7 +175,7 @@ dependencies { implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion" implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar' - implementation "com.github.CanHub:Android-Image-Cropper:4.1.0" + implementation "com.github.CanHub:Android-Image-Cropper:4.2.1" implementation "de.c1710:filemojicompat-ui:$filemojicompat_version" implementation "de.c1710:filemojicompat:$filemojicompat_version" 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 57723c721..c5bdf4654 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 @@ -23,6 +23,7 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager +import android.graphics.Bitmap import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter import android.net.Uri @@ -56,6 +57,9 @@ import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.transition.TransitionManager +import com.canhub.cropper.CropImage +import com.canhub.cropper.CropImageContract +import com.canhub.cropper.options import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.BaseActivity @@ -83,6 +87,7 @@ import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.afterTextChanged import com.keylesspalace.tusky.util.combineLiveData import com.keylesspalace.tusky.util.combineOptionalLiveData +import com.keylesspalace.tusky.util.getMediaSize import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.highlightSpans import com.keylesspalace.tusky.util.loadAvatar @@ -151,6 +156,32 @@ class ComposeActivity : } } + // Contract kicked off by editImageInQueue; expects viewModel.cropImageItemOld set + private val cropImage = registerForActivityResult(CropImageContract()) { result -> + val uriNew = result.uriContent + if (result.isSuccessful && uriNew != null) { + viewModel.cropImageItemOld?.let { itemOld -> + val size = getMediaSize(getApplicationContext().getContentResolver(), uriNew) + + lifecycleScope.launch { + viewModel.addMediaToQueue( + itemOld.type, + uriNew, + size, + itemOld.description, + itemOld + ) + } + } + } else if (result == CropImage.CancelledResult) { + Log.w("ComposeActivity", "Edit image cancelled by user") + } else { + Log.w("ComposeActivity", "Edit image failed: " + result.error) + displayTransientError(R.string.error_media_edit_failed) + } + viewModel.cropImageItemOld = null + } + public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -185,6 +216,7 @@ class ComposeActivity : viewModel.updateDescription(item.localId, newDescription) } }, + onEditImage = this::editImageInQueue, onRemove = this::removeMediaFromQueue ) binding.composeMediaPreviewBar.layoutManager = @@ -867,6 +899,27 @@ class ComposeActivity : binding.addPollTextActionTextView.compoundDrawablesRelative[0].colorFilter = PorterDuffColorFilter(textColor, PorterDuff.Mode.SRC_IN) } + private fun editImageInQueue(item: QueuedMedia) { + // If input image is lossless, output image should be lossless. + // Currently the only supported lossless format is png. + val mimeType: String? = contentResolver.getType(item.uri) + val isPng: Boolean = mimeType != null && mimeType.endsWith("/png") + val context = getApplicationContext() + val tempFile = createNewImageFile(context, if (isPng) ".png" else ".jpg") + + // "Authority" must be the same as the android:authorities string in AndroidManifest.xml + val uriNew = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", tempFile) + + viewModel.cropImageItemOld = item + + cropImage.launch( + options(uri = item.uri) { + setOutputUri(uriNew) + setOutputCompressFormat(if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG) + } + ) + } + private fun removeMediaFromQueue(item: QueuedMedia) { viewModel.removeMediaFromQueue(item) } 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 2c0da5833..b7726c38e 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 @@ -95,6 +95,9 @@ class ComposeViewModel @Inject constructor( private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty() + // Used in ComposeActivity to pass state to result function when cropImage contract inflight + var cropImageItemOld: QueuedMedia? = null + init { viewModelScope.launch { emoji.postValue(instanceInfoRepo.getEmojis()) @@ -122,13 +125,16 @@ class ComposeViewModel @Inject constructor( } } - private suspend fun addMediaToQueue( + suspend fun addMediaToQueue( type: QueuedMedia.Type, uri: Uri, mediaSize: Long, - description: String? = null + description: String? = null, + replaceItem: QueuedMedia? = null ): QueuedMedia { - val mediaItem = media.updateAndGet { mediaValue -> + var stashMediaItem: QueuedMedia? = null + + media.updateAndGet { mediaValue -> val mediaItem = QueuedMedia( localId = (mediaValue.maxOfOrNull { it.localId } ?: 0) + 1, uri = uri, @@ -136,8 +142,19 @@ class ComposeViewModel @Inject constructor( mediaSize = mediaSize, description = description ) - mediaValue + mediaItem - }.last() + stashMediaItem = mediaItem + + if (replaceItem != null) { + mediaToJob[replaceItem.localId]?.cancel() + mediaValue.map { + if (it.localId == replaceItem.localId) mediaItem else it + } + } else { // Append + mediaValue + mediaItem + } + } + val mediaItem = stashMediaItem!! // stashMediaItem is always non-null and uncaptured at this point, but Kotlin doesn't know that + mediaToJob[mediaItem.localId] = viewModelScope.launch { mediaUploader .uploadMedia(mediaItem) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt index 0b1fa8c41..be54a1aa9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt @@ -32,6 +32,7 @@ import com.keylesspalace.tusky.components.compose.view.ProgressImageView class MediaPreviewAdapter( context: Context, private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit, + private val onEditImage: (ComposeActivity.QueuedMedia) -> Unit, private val onRemove: (ComposeActivity.QueuedMedia) -> Unit ) : RecyclerView.Adapter() { @@ -43,12 +44,16 @@ class MediaPreviewAdapter( val item = differ.currentList[position] val popup = PopupMenu(view.context, view) val addCaptionId = 1 - val removeId = 2 + val editImageId = 2 + val removeId = 3 popup.menu.add(0, addCaptionId, 0, R.string.action_set_caption) + if (item.type == ComposeActivity.QueuedMedia.Type.IMAGE) + popup.menu.add(0, editImageId, 0, R.string.action_edit_image) popup.menu.add(0, removeId, 0, R.string.action_remove) popup.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { addCaptionId -> onAddCaption(item) + editImageId -> onEditImage(item) removeId -> onRemove(item) } 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 f1debc98b..b2915c799 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 @@ -54,14 +54,14 @@ sealed class UploadEvent { data class FinishedEvent(val mediaId: String) : UploadEvent() } -fun createNewImageFile(context: Context): File { +fun createNewImageFile(context: Context, suffix: String = ".jpg"): File { // Create an image file name val randomId = randomAlphanumericString(12) val imageFileName = "Tusky_${randomId}_" val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) return File.createTempFile( imageFileName, /* prefix */ - ".jpg", /* suffix */ + suffix, /* suffix */ storageDir /* directory */ ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 27902cefa..ad87b6fc8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,6 +14,7 @@ The file must be less than 8MB. Video files must be less than 40MB. Audio files must be less than 40MB. + The attachment could not be edited. That type of file cannot be uploaded. That file could not be opened. Permission to read media is required. @@ -404,6 +405,7 @@ Describe for visually impaired\n(%d character limit) Set caption + Edit image Remove Lock account Requires you to manually approve followers From fd9d48e4a67b7cc55383f34488ccf3a314ac8d20 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Wed, 25 May 2022 20:54:25 +0200 Subject: [PATCH 03/82] remove legacy notification channel cleanup (#2550) --- .../com/keylesspalace/tusky/SplashActivity.kt | 5 ----- .../notifications/NotificationHelper.java | 18 ------------------ 2 files changed, 23 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt index f69aa5d19..638f0e5be 100644 --- a/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/SplashActivity.kt @@ -20,7 +20,6 @@ import android.content.Intent import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import com.keylesspalace.tusky.components.login.LoginActivity -import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.di.Injectable import javax.inject.Inject @@ -34,12 +33,8 @@ class SplashActivity : AppCompatActivity(), Injectable { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - /** delete old notification channels */ - NotificationHelper.deleteLegacyNotificationChannels(this, accountManager) - /** Determine whether the user is currently logged in, and if so go ahead and load the * timeline. Otherwise, start the activity_login screen. */ - val intent = if (accountManager.activeAccount != null) { Intent(this, MainActivity::class.java) } else { 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 ce11ab1cc..45ecd0f65 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 @@ -458,24 +458,6 @@ public class NotificationHelper { } } - public static void deleteLegacyNotificationChannels(@NonNull Context context, @NonNull AccountManager accountManager) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - - NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - - // used until Tusky 1.4 - notificationManager.deleteNotificationChannel(CHANNEL_MENTION); - notificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE); - notificationManager.deleteNotificationChannel(CHANNEL_BOOST); - notificationManager.deleteNotificationChannel(CHANNEL_FOLLOW); - - // used until Tusky 1.7 - for(AccountEntity account: accountManager.getAllAccountsOrderedByActive()) { - notificationManager.deleteNotificationChannel(CHANNEL_FAVOURITE+" "+account.getIdentifier()); - } - } - } - public static boolean areNotificationsEnabled(@NonNull Context context, @NonNull AccountManager accountManager) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { From 9139e7bbf15b7b10eeade610cbfb8c4770c95569 Mon Sep 17 00:00:00 2001 From: Constantin A <10349490+C1710@users.noreply.github.com> Date: Wed, 25 May 2022 20:55:00 +0200 Subject: [PATCH 04/82] Set FilemojiCompat to version 3.2.2 (#2553) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 821a8e5cb..570bfe566 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -101,7 +101,7 @@ ext.glideVersion = '4.13.1' ext.daggerVersion = '2.42' ext.materialdrawerVersion = '8.4.5' ext.emoji2_version = '1.1.0' -ext.filemojicompat_version = '3.2.1' +ext.filemojicompat_version = '3.2.2' // if libraries are changed here, they should also be changed in LicenseActivity dependencies { From 2424dde9ccea8f94734d2bf6550c8f71166cd90c Mon Sep 17 00:00:00 2001 From: UlrichKu Date: Fri, 27 May 2022 18:43:10 +0200 Subject: [PATCH 05/82] Fire observable to update timestamps directly on resume (#2554) (#2555) --- .../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 541838884..96b438eba 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 @@ -471,7 +471,7 @@ class TimelineFragment : val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false) if (!useAbsoluteTime) { - Observable.interval(1, TimeUnit.MINUTES) + Observable.interval(0, 1, TimeUnit.MINUTES) .observeOn(AndroidSchedulers.mainThread()) .autoDispose(this, Lifecycle.Event.ON_PAUSE) .subscribe { From 55e99be1235ae44938d3cd4e90b727d718aac467 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Thu, 26 May 2022 19:40:41 +0000 Subject: [PATCH 06/82] Translated using Weblate (Ukrainian) Currently translated at 100.0% (485 of 485 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (485 of 485 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 | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 6e6924a58..3f49aaed9 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -550,4 +550,11 @@ Вхід Не вдалося завантажити сторінку входу. Збереження чернетки… + Відхилити + Подробиці + Увійдіть повторно, щоб отримувати push-сповіщення + Увійдіть повторно до всіх облікових записів, щоб увімкнути підтримку push-сповіщень. + Щоб використовувати push-сповіщення через UnifiedPush, Tusky потребує дозволу стежити за сповіщеннями на вашому сервері Mastodon. Це вимагає повторного входу, щоб змінити області OAuth, надані Tusky. Використання параметра повторного входу тут або в налаштуваннях облікового запису збереже всі ваші локальні чернетки та кеш. + Ви повторно увійшли до свого поточного облікового запису, щоб надати дозвіл на стеження Tusky. Однак у вас все ще є інші облікові записи, які не мігрували таким чином. Перейдіть до них і повторно увійдіть до них по одному, щоб забезпечити підтримку UnifiedPush сповіщень. + Приєднується %1$s \ No newline at end of file From 6fd76f76227c8595147794834407645b5ab19461 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 26 May 2022 19:40:41 +0000 Subject: [PATCH 07/82] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (488 of 488 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (486 of 486 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (485 of 485 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 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index eb1d8559c..d3d336a7f 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -536,4 +536,14 @@ 当你进行过互动的嘟文被编辑时发出通知 无法加载登录页。 正在保存草稿… + 为推送通知重新登录 + 不理会 + 详情 + 你已重新登录当前账户,向 Tusky 授予推送订阅权限。但是,你仍然有其他没有以这种方式迁移的账户。切换到它们,逐个重新登录,以启用 UnifiedPush 通知支持。 + 已加入 %1$s + 重新登录所有账户来启用推送通知支持。 + 为了通过 UnifiedPush 使用推送通知,Tusky 需要订阅你 Mastodon 服务器通知的权限。这需要重新登录来更改授予 Tusky 的 OAuth 作用域。使用此处或账户首选项中“重新登录”选项将保留你所有的本地草稿和缓存。 + 1+ + 无法编辑附件。 + 编辑图片 \ No newline at end of file From 9cdf1ced87eb56264a7551d3e8506fed697d8fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Thu, 26 May 2022 19:40:41 +0000 Subject: [PATCH 08/82] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (488 of 488 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (486 of 486 strings) Translated using Weblate (Vietnamese) Currently translated at 100.0% (485 of 485 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, 10 insertions(+) diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 6788526a4..5522d9964 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -517,4 +517,14 @@ Đăng nhập Không thể tải trang đăng nhập. Đang lưu nháp… + Bỏ qua + Đăng nhập lại để hiện thông báo đẩy + Chi tiết + Tham gia vào %1$s + Đăng nhập lại tất cả tài khoản để kích hoạt thông báo đẩy. + Bạn đã đăng nhập lại vào tài khoản hiện tại của mình để cấp quyền thông báo đẩy cho Tusky. Tuy nhiên, bạn vẫn có các tài khoản khác chưa kích hoạt thông báo đẩy theo cách này. Chuyển sang chúng và đăng nhập từng cái một để cho phép hỗ trợ thông báo UnifiedPush. + Để sử dụng thông báo đẩy qua UnifiedPush, Tusky cần có quyền đăng ký thông báo trên máy chủ Mastodon của bạn. Bạn hãy thoát ra rồi đăng nhập lại để thay đổi phạm vi OAuth được cấp cho Tusky. Sử dụng đăng nhập lại ở đây hoặc trong cài đặt Tài khoản sẽ bảo toàn tất cả các tút nháp và bộ nhớ đệm trên điện thoại của bạn. + 1+ + Tập tin đính kèm không thể chỉnh sửa. + Sửa ảnh \ No newline at end of file From d7d3c6a183ee8c2e86147640c8543b0627b9139c Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Thu, 26 May 2022 19:40:41 +0000 Subject: [PATCH 09/82] Translated using Weblate (French) Currently translated at 98.7% (479 of 485 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 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index b36cfa70c..1bcd01ce5 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -547,4 +547,7 @@ Messages modifiés Notifications quand un post avec lequel vous avez interagi est modifié Se connecter + Ici depuis %1$s + Détails + Sauvegarde du brouillon … \ No newline at end of file From 47e269fd0746fc2cacb118be6561ed7da0ac94de Mon Sep 17 00:00:00 2001 From: hebbeff Date: Thu, 26 May 2022 19:40:41 +0000 Subject: [PATCH 10/82] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (485 of 485 strings) Co-authored-by: hebbeff Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/ Translation: Tusky/Tusky --- app/src/main/res/values-zh-rCN/strings.xml | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index d3d336a7f..7427331cf 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -31,7 +31,7 @@ 已置顶 正在关注 关注者 - 收藏 + 喜欢 被隐藏的用户 被屏蔽的用户 关注请求 @@ -50,7 +50,7 @@ 还没有内容。 还没有内容,向下拉动即可刷新! %s 转嘟了你的嘟文 - %s 收藏了你的嘟文 + %s 喜欢了你的嘟文 %s 关注了你 举报 @%s 是否有更多信息需报告? @@ -58,8 +58,8 @@ 回复 转嘟 取消转嘟 - 收藏 - 取消收藏 + 喜欢 + 取消喜欢 更多 发表嘟文 登录 Mastodon 帐号 @@ -81,7 +81,7 @@ 个人资料 设置 帐户设置 - 收藏 + 喜欢 被隐藏的用户 被屏蔽的用户 关注请求 @@ -171,7 +171,7 @@ 被提及 有新的关注者 嘟文被转嘟 - 嘟文被收藏 + 嘟文被喜欢 投票已结束 外观 应用主题 @@ -215,8 +215,8 @@ 当有用户关注我时 转嘟 当我的嘟文被转发时通知 - 收藏 - 当有用户收藏了我的嘟文时通知 + 喜欢 + 当有用户喜欢了我的嘟文时 投票 当我参与的投票结束时 %s 提及了你 @@ -343,7 +343,7 @@ <b>%s</b> 次转嘟 转嘟 - 收藏 + 喜欢 %1$s %1$s 和 %2$s %1$s,%2$s 和 %3$d 等人 @@ -354,7 +354,7 @@ 内容警告:%s 没有描述信息 被转嘟 - 被收藏 + 被喜欢 公开 @@ -516,7 +516,7 @@ 即使您的账号未上锁,管理员 %1$s 认为您可能需要手动处理来自这些账号的关注请求。 删除此对话吗? 删除对话 - 收藏前显示确认对话框 + 喜欢前显示确认对话框 删除书签 30 天 60 天 @@ -536,11 +536,11 @@ 当你进行过互动的嘟文被编辑时发出通知 无法加载登录页。 正在保存草稿… - 为推送通知重新登录 + 重新登陆以启用通知推送 不理会 详情 你已重新登录当前账户,向 Tusky 授予推送订阅权限。但是,你仍然有其他没有以这种方式迁移的账户。切换到它们,逐个重新登录,以启用 UnifiedPush 通知支持。 - 已加入 %1$s + 加入于%1$s 重新登录所有账户来启用推送通知支持。 为了通过 UnifiedPush 使用推送通知,Tusky 需要订阅你 Mastodon 服务器通知的权限。这需要重新登录来更改授予 Tusky 的 OAuth 作用域。使用此处或账户首选项中“重新登录”选项将保留你所有的本地草稿和缓存。 1+ From 1546ea2e1fb957b8e25e1b312f01acd124dfb2ba Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Thu, 26 May 2022 19:40:41 +0000 Subject: [PATCH 11/82] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (488 of 488 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (486 of 486 strings) Translated using Weblate (Norwegian Bokmål) Currently translated at 100.0% (485 of 485 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 | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index 044da640c..37fda7652 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -527,4 +527,15 @@ Varslinger når et innlegg du har hatt en interaksjon med er redigert Innlogging Klarte ikke å laste innloggingssiden. + Logg inn på nytt for pushvarsler + Avvis + Detaljer + Ble med %1$s + Logg inn all konti på nytt for å skru på pushvarsler. + For å kunne sende pushvarsler via UnifiedPush trenger Tusky tillatelse til å abonnere på varsler på Mastodon-serveren. Dette krever at du logger inn på nytt. Ved å bruke muligheten til å logge inn på nytt her eller i kontoinstillinger vil alle lokale kladder være tilgjengelig også etter at du har logget inn på nytt. + Du har logget inn på nytt for å tillate Tusky til å sende pushvarsler, men du har fortsatt andre konti som ikke har fått den nødvendige tillatelsen. Bytt til dem og logg inn på nytt på samme måte for å skru på støtte for pushvarsler via UnifiedPush. + Lagrer kladd… + 1+ + Vedlegget kan ikke redigeres. + Rediger bilde \ No newline at end of file From 86470459b1495eb50bd0221e270d5e4cd5ccb01d Mon Sep 17 00:00:00 2001 From: GunChleoc Date: Thu, 26 May 2022 19:40:42 +0000 Subject: [PATCH 12/82] Translated using Weblate (Gaelic) Currently translated at 99.5% (486 of 488 strings) Translated using Weblate (Gaelic) Currently translated at 99.5% (483 of 485 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 39e7d5a32..992972899 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -556,4 +556,12 @@ Clàraich a-steach Cha b’ urrainn dhuinn duilleag a’ chlàraidh a-steach fhosgladh. A’ sàbhaladh na dreuchd… + Leig seachad + Fiosrachadh + Air ballrachd fhaighinn %1$s + Clàraich a-steach às ùr leis a h-uile cunntas a chur na taice ri brathan putaidh an comas. + Clàraich a-steach às ùr airson brathan putaidh + 1+ + Cha deach le deasachadh a’ cheanglachain. + Deasaich an dealbh \ No newline at end of file From d0937077a255dd820fd1cbe9e247f777c8046c29 Mon Sep 17 00:00:00 2001 From: XoseM Date: Thu, 26 May 2022 19:40:42 +0000 Subject: [PATCH 13/82] Translated using Weblate (Galician) Currently translated at 100.0% (485 of 485 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 | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 2b6be45eb..8b7f6687f 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -521,4 +521,18 @@ hai unha nova usuaria Rexistros Notificacións sobre novas usuarias + Foi editada unha publicación coa que interactuei + Edicións da publicación + Creada %1$s + Volver a conectar tódalas contas para activar as notificacións push. + Acceder + Notificacións cando son editadas publicacións coas que interactuaches + Para poder usar as notificacións push vía UnifiedPush, Tusky require o permiso para subscribirse ás notificacións do teu servidor Mastodon. É necesario volver a acceder para cambiar os ámbitos OAuth concedidos a Tusky. Usando aquí ou nas Preferencias da Conta a opción de volver a acceder conservarás os borradores locais e caché. + Volveches a acceder para obter as notificacións push en Tusky. Aínda así tes algunha outra conta que non foi migrada a este modo. Cambia a esas contas e volve a conectar unha a unha para activar o soporte para notificacións de UnifiedPush. + Volve a acceder para ter notificacións push + %s editou a publicación + Desbotar + Detalles + Non se puido cargar a páxina de inicio. + Gardando borrador… \ No newline at end of file From f156188d026a19fac64b4014fc20582711306695 Mon Sep 17 00:00:00 2001 From: mondstern Date: Thu, 26 May 2022 19:40:42 +0000 Subject: [PATCH 14/82] Translated using Weblate (German) Currently translated at 99.1% (482 of 486 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 | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 3b3399318..6be0be581 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -536,4 +536,9 @@ Anmelden Die Anmeldeseite konnte nicht geladen werden. Beitragsbearbeitungen + Neuanmeldung für Push-Benachrichtigungen + Ablehnen + Du hast dich erneut in dein aktuelles Konto eingeloggt, um Tusky die Genehmigung für Push-Abonnements zu erteilen. Du hast jedoch noch andere Konten, die nicht auf diese Weise migriert wurden. Wechsel zu diesen Konten und melde dich nacheinander neu an, um die Unterstützung für UnifiedPush-Benachrichtigungen zu aktivieren. + Um Push-Benachrichtigungen über UnifiedPush verwenden zu können, benötigt Tusky die Erlaubnis, Benachrichtigungen auf Ihrem Mastodon-Server zu abonnieren. Dies erfordert eine erneute Anmeldung, um die Tusky gewährten OAuth-Bereiche zu ändern. Wenn du die Option zum erneuten Einloggen hier oder in den Kontoeinstellungen verwendest, bleiben alle deine lokalen Entwürfe und der Cache erhalten. + Melde alle Konten neu an, um die Unterstützung für Push-Benachrichtigungen zu aktivieren. \ No newline at end of file From 40bd54d5bdb0c7c585985dad3c9657368225f6d4 Mon Sep 17 00:00:00 2001 From: "Gera, Zoltan" Date: Thu, 26 May 2022 19:40:42 +0000 Subject: [PATCH 15/82] Translated using Weblate (Hungarian) Currently translated at 100.0% (488 of 488 strings) Translated using Weblate (Hungarian) Currently translated at 99.5% (484 of 486 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 | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 887c821fc..1afc2e296 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -533,4 +533,17 @@ Bejegyzések szerkesztése Értesítések olyan bejegyzések szerkesztéséről, melyekkel már dolgod volt Bejegyzés Létrehozása + Bejelentkezés újra a leküldési értesítések érdekében + Elvetés + Részletek + Csatlakozva %1$s + Bejelentkezés újra minden fiókkal a leküldéses értesítések engedélyezése érdekében. + Bejelentkezés + 1+ + Nem tudtuk betölteni a bejelentkező oldalt. + Vázlat mentése… + A csatolmány nem szerkeszthető. + Ahhoz, hogy használhass leküldési értesítéseket a UnifiedPush szolgáltatással, a Tusky-nak fel kell iratkoznia az értesítésekre a Mastodon szervereden. Ehhez új bejelentkezésre van szükség, hogy a Tusky számára kiosztott OAuth jogosultságok megváltozzanak. Az újbóli bejelentkezés funkció használata itt vagy a Fiókbeállításoknál meg fogja őrizni a helyi piszkozataidat és a cache tartalmát. + Újra bejelentkeztél a fiókodba, hogy feliratkoztasd a Tusky-t a leküldési értesítések használatára. Ugyanakkor vannak még fiókjaid, melyek még nem lettek így migrálva. Válts át rájuk és jelentkezz be újra mindegyikben, hogy ezekben is engedélyezd a UnifiedPush értesítések támogatását. + Kép szerkesztése \ No newline at end of file From c3bac680aeec1bdd2ec9be58bbcf8c9f01a4d016 Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Thu, 26 May 2022 19:40:42 +0000 Subject: [PATCH 16/82] Translated using Weblate (Italian) Currently translated at 98.1% (477 of 486 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 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 7b775bebe..39d359629 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -153,8 +153,8 @@ Revocare la richiesta di seguire? Smettere di seguire questo account? Eliminare questo post\? - Pubblico: visibile sulla timeline pubblica - Non in elenco: non visibile sulla timeline pubblica e locale + Pubblico: visibile sulle timeline pubbliche + Non in elenco: non visibile sulle timeline pubbliche Solo follower: visibile solo dai tuoi follower Diretto: visibile solo agli utenti menzionati Notifiche @@ -402,7 +402,7 @@ Scegli lista Lista Azioni per l\'immagine %s - Un sondaggio che hai votato si è concluso + Un sondaggio in cui hai votato si è concluso Un sondaggio che hai creato si è concluso %d giorno rimasto From eef7ce4bb76273e9063a214acc036b96e809d867 Mon Sep 17 00:00:00 2001 From: Bruno Miguel Date: Thu, 26 May 2022 19:40:42 +0000 Subject: [PATCH 17/82] Translated using Weblate (Portuguese (Portugal)) Currently translated at 97.7% (477 of 488 strings) Translated using Weblate (Portuguese (Portugal)) Currently translated at 98.1% (477 of 486 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 | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index fa4b458e3..e7d565590 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -65,10 +65,10 @@ Anúncios Licenças \@%s - %s fez boost + %s deu boost Nada aqui. Nada para ver aqui. Arraste para baixo para atualizar! - %s fez boost ao seu toot + %s deu boost ao seu toot %s adicionou o seu toot aos favoritos %s está a seguir-te %s pediu para te seguir @@ -134,7 +134,7 @@ Abrir menu Pesquisar Rascunhos - Toots agendados + Toots Agendados Privacidade do toot Aviso de conteúdo Teclado de emojis @@ -325,7 +325,6 @@ Listas Não foi possível renomear a lista Listas - Não foi possível criar a lista Não foi possível apagar a lista Criar uma lista From 3020ea59dc53e2ada82eef850077a62d9818e856 Mon Sep 17 00:00:00 2001 From: Luca Date: Thu, 26 May 2022 19:40:42 +0000 Subject: [PATCH 18/82] Translated using Weblate (Italian) Currently translated at 98.7% (482 of 488 strings) Co-authored-by: Luca Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/it/ Translation: Tusky/Tusky --- app/src/main/res/values-it/strings.xml | 45 ++++++++++++++------------ 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 39d359629..5a1de3a5f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -30,7 +30,7 @@ Con risposte Fissati Seguiti - Seguono + Seguaci Preferiti Utenti silenziati Utenti bloccati @@ -39,7 +39,7 @@ Bozze Licenze \@%s - %s ha boostato + %s ha condiviso Contenuto sensibile Media nascosto Clicca per visualizzare @@ -49,15 +49,15 @@ Riduci Qui non c\'è nulla. Qui non c\'è nulla. Trascina verso il basso per aggiornare! - %s ha boostato il tuo post + %s ha condiviso il tuo post %s ha messo il tuo post nei preferiti %s ti ha seguito Segnala @%s Commenti aggiuntivi? Risposta veloce Rispondi - Boosta - Rimuovi boost + Condividi + Rimuovi condivisione Aggiungi ai preferiti Rimuovi preferito Di più @@ -69,8 +69,8 @@ Smetti di seguire Blocca Sblocca - Nascondi boost - Mostra boost + Nascondi condivisioni + Mostra condivisioni Segnala Elimina TOOT @@ -109,8 +109,8 @@ Collegamenti Menzioni Hashtag - Vai all\'autore del boost - Mostra boost + Vai all\'autore della condivisione + Mostra condivisioni Mostra preferiti Hashtag Menzioni @@ -166,7 +166,7 @@ Notificami quando vengo menzionato vengo seguito - i miei post vengono boostati + i miei post vengono condivisi i miei post vengono messi nei preferiti Aspetto Tema dell\'app @@ -183,7 +183,7 @@ Lingua Filtraggio della timeline Schede - Mostra boost + Mostra condivisioni Mostra risposte Mostra anteprime media Proxy @@ -208,8 +208,8 @@ Notifiche di quando vieni menzionato da qualcuno Nuovi follower Notifiche su nuovi follower - Boost - Notifiche sui tuoi post che vengono boostati + Condivisioni + Notifiche sui tuoi post che vengono condivisi Preferiti Notifiche sui tuoi post che vengono segnati come preferiti %s ti ha menzionato @@ -312,8 +312,8 @@ Download fallito Bot %1$s si è spostato su: - Boost con la visibilità del post di origine - Annulla boost + Condividi con la visibilità del post originale + Annulla condivisione Tusky contiene codice e risorse dai seguenti progetti open source: Licenziata sotto la Licenza Apache (copia sotto) CC-BY 4.0 @@ -334,7 +334,7 @@ <b>%s</b> Boost <b>%s</b> Boost - Boostato da + Condiviso da Aggiunto ai preferiti da %1$s %1$s e %2$s @@ -470,7 +470,7 @@ Salvato! La tua nota privata su questo account Nascondi il titolo della barra degli strumenti in alto - Mostra la finestra di conferma prima di boostare + Mostra la finestra di conferma prima di condividere Mostra le anteprime dei collegamenti nelle timelines Mastodon ha un intervallo di programmazione minimo di 5 minuti. Non ci sono annunci. @@ -515,13 +515,13 @@ 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 post +\n - Notifiche riguardo a Preferiti/Condivisioni/Following +\n - Conteggio dei Preferiti/Condivisioni nei post \n - Statistiche riguardo a Preferiti/Post nei profili \n \n Le notifiche push non saranno influenzate, ma puoi modificare le tue impostazioni delle notifiche manualmente. Rimuovi segnalibro - Chiedi conferma prima di boostare + Chiedi conferma prima di condividere 14 giorni 30 giorni 60 giorni @@ -540,4 +540,9 @@ Modifiche ai post Notifiche di quando i post con cui hai interagito vengono modificati Non è stato possibile caricare la pagina di login. + L\'allegato non può essere modificato. + Modifica immagine + Salvataggio bozza… + Scartare + Dettagli \ No newline at end of file From e04a6fea323cca99678abd489d8cbf20352a1433 Mon Sep 17 00:00:00 2001 From: XoseM Date: Thu, 26 May 2022 19:40:41 +0000 Subject: [PATCH 19/82] Translated using Weblate (Galician) Currently translated at 100.0% (17 of 17 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/gl/ --- fastlane/metadata/android/gl/changelogs/91.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 fastlane/metadata/android/gl/changelogs/91.txt diff --git a/fastlane/metadata/android/gl/changelogs/91.txt b/fastlane/metadata/android/gl/changelogs/91.txt new file mode 100644 index 000000000..00017693d --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Soporte para o novos tipos de notificación de Mastodon 3.5 +- A insignia de bot foi redeseñada e combina mellor co decorado seleccionado +- Podes seleccionar texto na vista de detalles da publicación +- Moitos arranxos adicionais, incluíndo o que non permitía acceder en Android <6 From 280bdfab982b2ac0d81684f72576924fec346970 Mon Sep 17 00:00:00 2001 From: "Gera, Zoltan" Date: Thu, 26 May 2022 19:40:41 +0000 Subject: [PATCH 20/82] Translated using Weblate (Hungarian) Currently translated at 100.0% (17 of 17 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/hu/ --- fastlane/metadata/android/hu/changelogs/91.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 fastlane/metadata/android/hu/changelogs/91.txt diff --git a/fastlane/metadata/android/hu/changelogs/91.txt b/fastlane/metadata/android/hu/changelogs/91.txt new file mode 100644 index 000000000..c9ad649e1 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Támogatás az új Mastodon 3.5 értesítési típusokhoz +- A bot jelvény jobban néz ki és alkalmazkodik a választott témához +- A szöveget már kiválaszthatod a bejegyzési részletek megtekintésénél is +- Sok hibajavítás, beleértve egy olyat, mely megakadályozta a bejelentkezést Android 6-on vagy alatta From becb6771769dcad5f4169e37127b406688d165da Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Fri, 27 May 2022 18:44:16 +0200 Subject: [PATCH 21/82] make WebView debuggable in debug builds (#2548) --- .../tusky/components/login/LoginWebViewActivity.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 2ed38720b..07bd56529 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 @@ -83,6 +83,10 @@ class LoginWebViewActivity : BaseActivity(), Injectable { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + if (BuildConfig.DEBUG) { + WebView.setWebContentsDebuggingEnabled(true) + } + val data = OauthLogin.parseData(intent) setContentView(binding.root) From d2befa83d3cb9af4c3fb4c10deb1581c36c85e80 Mon Sep 17 00:00:00 2001 From: UlrichKu Date: Fri, 27 May 2022 19:51:22 +0200 Subject: [PATCH 22/82] 2554 refresh timestamps on resume (#2562) * Fire observable to update timestamps directly on resume (#2554) * Fire observable to update timestamps directly on resume (#2554) --- .../com/keylesspalace/tusky/fragment/NotificationsFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 94ee496e9..080ba4ca0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -1231,7 +1231,7 @@ public class NotificationsFragment extends SFragment implements SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity()); boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false); if (!useAbsoluteTime) { - Observable.interval(1, TimeUnit.MINUTES) + Observable.interval(0, 1, TimeUnit.MINUTES) .observeOn(AndroidSchedulers.mainThread()) .to(autoDisposable(from(this, Lifecycle.Event.ON_PAUSE))) .subscribe( From 06e32f703a1facf629158c5a2d56fb218f228fe3 Mon Sep 17 00:00:00 2001 From: mcclure Date: Sun, 29 May 2022 13:21:33 -0400 Subject: [PATCH 23/82] Fix unintended [mismatched] show-replies preference (with key force-reset) (#2568) * Fix unintended [mismatched] show-replies preference and add a comment to prevent confusion. * Change the key on TAB_FILTER_HOME_REPLIES to reset everyone's value here once. --- .../tusky/components/preference/TabFilterPreferencesFragment.kt | 2 +- .../tusky/components/timeline/viewmodel/TimelineViewModel.kt | 1 + .../java/com/keylesspalace/tusky/settings/SettingsConstants.kt | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt index 71c5e10ec..c0a763294 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt @@ -39,7 +39,7 @@ class TabFilterPreferencesFragment : PreferenceFragmentCompat() { checkBoxPreference { setTitle(R.string.pref_title_show_replies) key = PrefKeys.TAB_FILTER_HOME_REPLIES - setDefaultValue(false) + setDefaultValue(true) isIconSpaceReserved = false } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt index 544d08181..75fa503c5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt @@ -81,6 +81,7 @@ abstract class TimelineViewModel( this.tags = tags if (kind == Kind.HOME) { + // Note the variable is "true if filter" but the underlying preference/settings text is "true if show" filterRemoveReplies = !sharedPreferences.getBoolean(PrefKeys.TAB_FILTER_HOME_REPLIES, true) filterRemoveReblogs = 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 6540601a6..ee92fc2d7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt @@ -62,6 +62,6 @@ object PrefKeys { 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_REPLIES = "tabFilterHomeReplies_v2" // This was changed once to reset an unintentionally set default. const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts" } From 579f0eb833fa35114dbfcf9472378139c2f6fb34 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 29 May 2022 19:21:44 +0200 Subject: [PATCH 24/82] update android animation glide plugin to 2.22.0 (#2557) --- app/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 570bfe566..54e3452dc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -152,7 +152,7 @@ dependencies { implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion" kapt "com.github.bumptech.glide:compiler:$glideVersion" - implementation "com.github.penfeizhou.android.animation:glide-plugin:2.20.0" + implementation "com.github.penfeizhou.android.animation:glide-plugin:2.22.0" implementation "io.reactivex.rxjava3:rxjava:3.1.3" implementation "io.reactivex.rxjava3:rxandroid:3.0.0" From e63cd68baf49b28afc039da340407f9e406beb78 Mon Sep 17 00:00:00 2001 From: Ivan Kupalov Date: Sun, 29 May 2022 19:22:59 +0200 Subject: [PATCH 25/82] Fix filters in timelines in a simple way, fix #2546 (#2566) Loading of statuses and loading of filters is an "intended" race: we want to display statuses first, especially if they are cached. Unfortunately we do not cache filters themselves so when we load cached statuses we do not apply filters. One part of the solution is to re-filter the statuses once we fetch the filters. This commit implements it. Caching of filters is not included yet. --- .../viewmodel/CachedTimelineViewModel.kt | 61 +++++++++++++++---- .../viewmodel/NetworkTimelineViewModel.kt | 4 ++ .../timeline/viewmodel/TimelineViewModel.kt | 6 ++ 3 files changed, 59 insertions(+), 12 deletions(-) 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 7158a7b3a..a3fd4ec08 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 @@ -21,6 +21,7 @@ import androidx.lifecycle.viewModelScope import androidx.paging.ExperimentalPagingApi import androidx.paging.Pager import androidx.paging.PagingConfig +import androidx.paging.PagingSource import androidx.paging.cachedIn import androidx.paging.filter import androidx.paging.map @@ -37,6 +38,7 @@ import com.keylesspalace.tusky.components.timeline.toViewData import com.keylesspalace.tusky.components.timeline.util.ifExpected import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.TimelineStatusWithAccount import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.MastodonApi @@ -66,7 +68,16 @@ class CachedTimelineViewModel @Inject constructor( filterModel: FilterModel, private val db: AppDatabase, private val gson: Gson -) : TimelineViewModel(timelineCases, api, eventHub, accountManager, sharedPreferences, filterModel) { +) : TimelineViewModel( + timelineCases, + api, + eventHub, + accountManager, + sharedPreferences, + filterModel +) { + + private var currentPagingSource: PagingSource? = null @OptIn(ExperimentalPagingApi::class) override val statuses = Pager( @@ -78,6 +89,8 @@ class CachedTimelineViewModel @Inject constructor( EmptyTimelinePagingSource() } else { db.timelineDao().getStatuses(activeAccount.id) + }.also { newPagingSource -> + this.currentPagingSource = newPagingSource } } ).flow @@ -113,13 +126,15 @@ class CachedTimelineViewModel @Inject constructor( override fun changeContentShowing(isShowing: Boolean, status: StatusViewData.Concrete) { viewModelScope.launch { - db.timelineDao().setContentShowing(accountManager.activeAccount!!.id, status.id, isShowing) + db.timelineDao() + .setContentShowing(accountManager.activeAccount!!.id, status.id, isShowing) } } override fun changeContentCollapsed(isCollapsed: Boolean, status: StatusViewData.Concrete) { viewModelScope.launch { - db.timelineDao().setContentCollapsed(accountManager.activeAccount!!.id, status.id, isCollapsed) + db.timelineDao() + .setContentCollapsed(accountManager.activeAccount!!.id, status.id, isCollapsed) } } @@ -146,12 +161,21 @@ class CachedTimelineViewModel @Inject constructor( val activeAccount = accountManager.activeAccount!! - timelineDao.insertStatus(Placeholder(placeholderId, loading = true).toEntity(activeAccount.id)) + timelineDao.insertStatus( + Placeholder(placeholderId, loading = true).toEntity( + activeAccount.id + ) + ) val response = db.withTransaction { val idAbovePlaceholder = timelineDao.getIdAbove(activeAccount.id, placeholderId) - val nextPlaceholderId = timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId) - api.homeTimeline(maxId = idAbovePlaceholder, sinceId = nextPlaceholderId, limit = LOAD_AT_ONCE) + val nextPlaceholderId = + timelineDao.getNextPlaceholderIdAfter(activeAccount.id, placeholderId) + api.homeTimeline( + maxId = idAbovePlaceholder, + sinceId = nextPlaceholderId, + limit = LOAD_AT_ONCE + ) }.await() val statuses = response.body() @@ -165,16 +189,21 @@ class CachedTimelineViewModel @Inject constructor( timelineDao.delete(activeAccount.id, placeholderId) val overlappedStatuses = if (statuses.isNotEmpty()) { - timelineDao.deleteRange(activeAccount.id, statuses.last().id, statuses.first().id) + timelineDao.deleteRange( + activeAccount.id, + statuses.last().id, + statuses.first().id + ) } else { 0 } for (status in statuses) { timelineDao.insertAccount(status.account.toEntity(activeAccount.id, gson)) - status.reblog?.account?.toEntity(activeAccount.id, gson)?.let { rebloggedAccount -> - timelineDao.insertAccount(rebloggedAccount) - } + status.reblog?.account?.toEntity(activeAccount.id, gson) + ?.let { rebloggedAccount -> + timelineDao.insertAccount(rebloggedAccount) + } timelineDao.insertStatus( status.toEntity( timelineUserId = activeAccount.id, @@ -193,7 +222,10 @@ class CachedTimelineViewModel @Inject constructor( to guarantee the placeholder has an id that exists on the server as not all servers handle client generated ids as expected */ timelineDao.insertStatus( - Placeholder(statuses.last().id, loading = false).toEntity(activeAccount.id) + Placeholder( + statuses.last().id, + loading = false + ).toEntity(activeAccount.id) ) } } @@ -208,7 +240,8 @@ class CachedTimelineViewModel @Inject constructor( private suspend fun loadMoreFailed(placeholderId: String, e: Exception) { Log.w("CachedTimelineVM", "failed loading statuses", e) val activeAccount = accountManager.activeAccount!! - db.timelineDao().insertStatus(Placeholder(placeholderId, loading = false).toEntity(activeAccount.id)) + db.timelineDao() + .insertStatus(Placeholder(placeholderId, loading = false).toEntity(activeAccount.id)) } override fun handleReblogEvent(reblogEvent: ReblogEvent) { @@ -234,6 +267,10 @@ class CachedTimelineViewModel @Inject constructor( } } + override fun invalidate() { + currentPagingSource?.invalidate() + } + companion object { private const val MAX_STATUSES_IN_CACHE = 1000 } 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 ca7988bb9..5c2b4acda 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 @@ -249,6 +249,10 @@ class NetworkTimelineViewModel @Inject constructor( currentSource?.invalidate() } + override fun invalidate() { + currentSource?.invalidate() + } + @Throws(IOException::class, HttpException::class) suspend fun fetchStatusesForKind( fromId: String?, diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt index 75fa503c5..79fe885ac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt @@ -173,6 +173,9 @@ abstract class TimelineViewModel( abstract fun fullReload() + /** Triggered when currently displayed data must be reloaded. */ + protected abstract fun invalidate() + protected fun shouldFilterStatus(statusViewData: StatusViewData): Boolean { val status = statusViewData.asStatusOrNull()?.status ?: return false return status.inReplyToId != null && filterRemoveReplies || @@ -288,6 +291,9 @@ abstract class TimelineViewModel( filterContextMatchesKind(kind, it.context) } ) + // After the filters are loaded we need to reload displayed content to apply them. + // It can happen during the usage or at startup, when we get statuses before filters. + invalidate() } } From 434d345d0138d8aba67a66d8d47665f45bc26cff Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Sun, 29 May 2022 19:23:08 +0200 Subject: [PATCH 26/82] remove unused member vars from TimelineFragment (#2561) --- .../tusky/components/timeline/TimelineFragment.kt | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) 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 96b438eba..bdc778128 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 @@ -103,9 +103,6 @@ class TimelineFragment : private lateinit var adapter: TimelinePagingAdapter private var isSwipeToRefreshEnabled = true - - private var layoutManager: LinearLayoutManager? = null - private var scrollListener: RecyclerView.OnScrollListener? = null private var hideFab = false override fun onCreate(savedInstanceState: Bundle?) { @@ -226,7 +223,7 @@ class TimelineFragment : if (actionButtonPresent()) { val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) hideFab = preferences.getBoolean("fabHide", false) - scrollListener = object : RecyclerView.OnScrollListener() { + binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { val composeButton = (activity as ActionButtonActivity).actionButton if (composeButton != null) { @@ -241,9 +238,7 @@ class TimelineFragment : } } } - }.also { - binding.recyclerView.addOnScrollListener(it) - } + }) } eventHub.events @@ -279,8 +274,7 @@ class TimelineFragment : } ) binding.recyclerView.setHasFixedSize(true) - layoutManager = LinearLayoutManager(context) - binding.recyclerView.layoutManager = layoutManager + binding.recyclerView.layoutManager = LinearLayoutManager(context) val divider = DividerItemDecoration(context, RecyclerView.VERTICAL) binding.recyclerView.addItemDecoration(divider) @@ -482,7 +476,7 @@ class TimelineFragment : override fun onReselect() { if (isAdded) { - layoutManager!!.scrollToPosition(0) + binding.recyclerView.layoutManager?.scrollToPosition(0) binding.recyclerView.stopScroll() } } From 2983c3f48ea8da8be63aa368b42d52b8dd2993b9 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 30 May 2022 18:15:17 +0200 Subject: [PATCH 27/82] Add UtcDateTypeAdapter for Gson (#2549) * Add UtcDateTypeAdapter for Gson * add 38.json --- .../38.json | 869 ++++++++++++++++++ .../keylesspalace/tusky/db/AppDatabase.java | 12 +- .../com/keylesspalace/tusky/di/AppModule.kt | 2 +- .../keylesspalace/tusky/di/NetworkModule.kt | 7 +- .../tusky/json/UtcDateTypeAdapter.java | 284 ++++++ 5 files changed, 1171 insertions(+), 3 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/38.json create mode 100644 app/src/main/java/com/keylesspalace/tusky/json/UtcDateTypeAdapter.java diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/38.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/38.json new file mode 100644 index 000000000..dacfb708b --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/38.json @@ -0,0 +1,869 @@ +{ + "formatVersion": 1, + "database": { + "version": 38, + "identityHash": "11033751d382aa8a1c6fc68833097d35", + "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, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, 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, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '11033751d382aa8a1c6fc68833097d35')" + ] + } +} \ No newline at end of file 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 c028c88a8..bdbfb09cc 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 = 37) + }, version = 38) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -561,4 +561,14 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_repliesCount` INTEGER NOT NULL DEFAULT 0"); } }; + + public static final Migration MIGRATION_37_38 = new Migration(37, 38) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + + // no actual scheme change, but timestamps are now serialized differently so all cache tables that contain them need to be cleaned + database.execSQL("DELETE FROM `TimelineStatusEntity`"); + database.execSQL("DELETE FROM `ConversationEntity`"); + } + }; } 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 08069ae88..b14c53922 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -64,7 +64,7 @@ class AppModule { 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_34_35, - AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, + AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38 ) .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 90dd30263..c49fbd132 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -20,8 +20,10 @@ import android.content.SharedPreferences import android.os.Build 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.UtcDateTypeAdapter import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MediaUploadApi @@ -38,6 +40,7 @@ import retrofit2.converter.gson.GsonConverterFactory import retrofit2.create import java.net.InetSocketAddress import java.net.Proxy +import java.util.Date import java.util.concurrent.TimeUnit import javax.inject.Singleton @@ -50,7 +53,9 @@ class NetworkModule { @Provides @Singleton - fun providesGson() = Gson() + fun providesGson(): Gson = GsonBuilder() + .registerTypeAdapter(Date::class.java, UtcDateTypeAdapter()) + .create() @Provides @Singleton diff --git a/app/src/main/java/com/keylesspalace/tusky/json/UtcDateTypeAdapter.java b/app/src/main/java/com/keylesspalace/tusky/json/UtcDateTypeAdapter.java new file mode 100644 index 000000000..d178b3fc9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/json/UtcDateTypeAdapter.java @@ -0,0 +1,284 @@ +// https://github.com/google/gson/blob/master/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java + +/* + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.keylesspalace.tusky.json; + +import java.io.IOException; +import java.text.ParseException; +import java.text.ParsePosition; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; + +import com.google.gson.JsonParseException; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; + +public final class UtcDateTypeAdapter extends TypeAdapter { + private final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC"); + + @Override + public void write(JsonWriter out, Date date) throws IOException { + if (date == null) { + out.nullValue(); + } else { + String value = format(date, true, UTC_TIME_ZONE); + out.value(value); + } + } + + @Override + public Date read(JsonReader in) throws IOException { + try { + switch (in.peek()) { + case NULL: + in.nextNull(); + return null; + default: + String date = in.nextString(); + // Instead of using iso8601Format.parse(value), we use Jackson's date parsing + // This is because Android doesn't support XXX because it is JDK 1.6 + return parse(date, new ParsePosition(0)); + } + } catch (ParseException e) { + throw new JsonParseException(e); + } + } + + // Date parsing code from Jackson databind ISO8601Utils.java + // https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java + private static final String GMT_ID = "GMT"; + + /** + * Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm] + * + * @param date the date to format + * @param millis true to include millis precision otherwise false + * @param tz timezone to use for the formatting (GMT will produce 'Z') + * @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm] + */ + private static String format(Date date, boolean millis, TimeZone tz) { + Calendar calendar = new GregorianCalendar(tz, Locale.US); + calendar.setTime(date); + + // estimate capacity of buffer as close as we can (yeah, that's pedantic ;) + int capacity = "yyyy-MM-ddThh:mm:ss".length(); + capacity += millis ? ".sss".length() : 0; + capacity += tz.getRawOffset() == 0 ? "Z".length() : "+hh:mm".length(); + StringBuilder formatted = new StringBuilder(capacity); + + padInt(formatted, calendar.get(Calendar.YEAR), "yyyy".length()); + formatted.append('-'); + padInt(formatted, calendar.get(Calendar.MONTH) + 1, "MM".length()); + formatted.append('-'); + padInt(formatted, calendar.get(Calendar.DAY_OF_MONTH), "dd".length()); + formatted.append('T'); + padInt(formatted, calendar.get(Calendar.HOUR_OF_DAY), "hh".length()); + formatted.append(':'); + padInt(formatted, calendar.get(Calendar.MINUTE), "mm".length()); + formatted.append(':'); + padInt(formatted, calendar.get(Calendar.SECOND), "ss".length()); + if (millis) { + formatted.append('.'); + padInt(formatted, calendar.get(Calendar.MILLISECOND), "sss".length()); + } + + int offset = tz.getOffset(calendar.getTimeInMillis()); + if (offset != 0) { + int hours = Math.abs((offset / (60 * 1000)) / 60); + int minutes = Math.abs((offset / (60 * 1000)) % 60); + formatted.append(offset < 0 ? '-' : '+'); + padInt(formatted, hours, "hh".length()); + formatted.append(':'); + padInt(formatted, minutes, "mm".length()); + } else { + formatted.append('Z'); + } + + return formatted.toString(); + } + /** + * Zero pad a number to a specified length + * + * @param buffer buffer to use for padding + * @param value the integer value to pad if necessary. + * @param length the length of the string we should zero pad + */ + private static void padInt(StringBuilder buffer, int value, int length) { + String strValue = Integer.toString(value); + for (int i = length - strValue.length(); i > 0; i--) { + buffer.append('0'); + } + buffer.append(strValue); + } + + /** + * Parse a date from ISO-8601 formatted string. It expects a format + * [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]] + * + * @param date ISO string to parse in the appropriate format. + * @param pos The position to start parsing from, updated to where parsing stopped. + * @return the parsed date + * @throws ParseException if the date is not in the appropriate format + */ + private static Date parse(String date, ParsePosition pos) throws ParseException { + Exception fail = null; + try { + int offset = pos.getIndex(); + + // extract year + int year = parseInt(date, offset, offset += 4); + if (checkOffset(date, offset, '-')) { + offset += 1; + } + + // extract month + int month = parseInt(date, offset, offset += 2); + if (checkOffset(date, offset, '-')) { + offset += 1; + } + + // extract day + int day = parseInt(date, offset, offset += 2); + // default time value + int hour = 0; + int minutes = 0; + int seconds = 0; + int milliseconds = 0; // always use 0 otherwise returned date will include millis of current time + if (checkOffset(date, offset, 'T')) { + + // extract hours, minutes, seconds and milliseconds + hour = parseInt(date, offset += 1, offset += 2); + if (checkOffset(date, offset, ':')) { + offset += 1; + } + + minutes = parseInt(date, offset, offset += 2); + if (checkOffset(date, offset, ':')) { + offset += 1; + } + // second and milliseconds can be optional + if (date.length() > offset) { + char c = date.charAt(offset); + if (c != 'Z' && c != '+' && c != '-') { + seconds = parseInt(date, offset, offset += 2); + // milliseconds can be optional in the format + if (checkOffset(date, offset, '.')) { + milliseconds = parseInt(date, offset += 1, offset += 3); + } + } + } + } + + // extract timezone + String timezoneId; + if (date.length() <= offset) { + throw new IllegalArgumentException("No time zone indicator"); + } + char timezoneIndicator = date.charAt(offset); + if (timezoneIndicator == '+' || timezoneIndicator == '-') { + String timezoneOffset = date.substring(offset); + timezoneId = GMT_ID + timezoneOffset; + offset += timezoneOffset.length(); + } else if (timezoneIndicator == 'Z') { + timezoneId = GMT_ID; + offset += 1; + } else { + throw new IndexOutOfBoundsException("Invalid time zone indicator " + timezoneIndicator); + } + + TimeZone timezone = TimeZone.getTimeZone(timezoneId); + if (!timezone.getID().equals(timezoneId)) { + throw new IndexOutOfBoundsException(); + } + + Calendar calendar = new GregorianCalendar(timezone); + calendar.setLenient(false); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, month - 1); + calendar.set(Calendar.DAY_OF_MONTH, day); + calendar.set(Calendar.HOUR_OF_DAY, hour); + calendar.set(Calendar.MINUTE, minutes); + calendar.set(Calendar.SECOND, seconds); + calendar.set(Calendar.MILLISECOND, milliseconds); + + pos.setIndex(offset); + return calendar.getTime(); + // If we get a ParseException it'll already have the right message/offset. + // Other exception types can convert here. + } catch (IndexOutOfBoundsException e) { + fail = e; + } catch (NumberFormatException e) { + fail = e; + } catch (IllegalArgumentException e) { + fail = e; + } + String input = (date == null) ? null : ("'" + date + "'"); + throw new ParseException("Failed to parse date [" + input + "]: " + fail.getMessage(), pos.getIndex()); + } + + /** + * Check if the expected character exist at the given offset in the value. + * + * @param value the string to check at the specified offset + * @param offset the offset to look for the expected character + * @param expected the expected character + * @return true if the expected character exist at the given offset + */ + private static boolean checkOffset(String value, int offset, char expected) { + return (offset < value.length()) && (value.charAt(offset) == expected); + } + + /** + * Parse an integer located between 2 given offsets in a string + * + * @param value the string to parse + * @param beginIndex the start index for the integer in the string + * @param endIndex the end index for the integer in the string + * @return the int + * @throws NumberFormatException if the value is not a number + */ + private static int parseInt(String value, int beginIndex, int endIndex) throws NumberFormatException { + if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) { + throw new NumberFormatException(value); + } + // use same logic as in Integer.parseInt() but less generic we're not supporting negative values + int i = beginIndex; + int result = 0; + int digit; + if (i < endIndex) { + digit = Character.digit(value.charAt(i++), 10); + if (digit < 0) { + throw new NumberFormatException("Invalid number: " + value); + } + result = -digit; + } + while (i < endIndex) { + digit = Character.digit(value.charAt(i++), 10); + if (digit < 0) { + throw new NumberFormatException("Invalid number: " + value); + } + result *= 10; + result -= digit; + } + return -result; + } +} From 131309e99cfe261f3fef3e94984a3ee69b35cf1d Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 30 May 2022 19:06:14 +0200 Subject: [PATCH 28/82] Fix conversations (#2556) * fix conversations * cleanup ConversationsRemoteMediator * update conversation timestamps regularly * improve loadStateListener * add db migration * make deleting from conversation db suspending * reorganize code in ConversationsFragment * delete NetworkStateViewHolder * cleanup imports * add 38.json * honor fabHide setting in ConversationsFragment * set page size to 30 --- .../38.json | 12 +- .../tusky/adapter/NetworkStateViewHolder.kt | 42 ---- .../conversation/ConversationAdapter.kt | 35 +++- .../conversation/ConversationEntity.kt | 9 +- .../ConversationLoadStateAdapter.kt | 25 ++- .../conversation/ConversationViewData.kt | 2 + .../conversation/ConversationViewHolder.java | 104 ++++++---- .../conversation/ConversationsFragment.kt | 192 ++++++++++++------ .../ConversationsRemoteMediator.kt | 60 ++++-- .../conversation/ConversationsRepository.kt | 11 +- .../conversation/ConversationsViewModel.kt | 2 +- .../keylesspalace/tusky/db/AppDatabase.java | 8 +- .../tusky/db/ConversationsDao.kt | 8 +- .../tusky/network/MastodonApi.kt | 4 +- 14 files changed, 314 insertions(+), 200 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/38.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/38.json index dacfb708b..391d6b862 100644 --- a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/38.json +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/38.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 38, - "identityHash": "11033751d382aa8a1c6fc68833097d35", + "identityHash": "798fc8d34064eb671c079689d4650ea5", "entities": [ { "tableName": "DraftEntity", @@ -690,7 +690,7 @@ }, { "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_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", "fields": [ { "fieldPath": "accountId", @@ -704,6 +704,12 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, { "fieldPath": "accounts", "columnName": "accounts", @@ -863,7 +869,7 @@ "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, '11033751d382aa8a1c6fc68833097d35')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '798fc8d34064eb671c079689d4650ea5')" ] } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt deleted file mode 100644 index cf7559908..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NetworkStateViewHolder.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* Copyright 2019 Conny Duck - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.adapter - -import androidx.paging.LoadState -import androidx.recyclerview.widget.RecyclerView -import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding -import com.keylesspalace.tusky.util.visible - -class NetworkStateViewHolder( - private val binding: ItemNetworkStateBinding, - private val retryCallback: () -> Unit -) : RecyclerView.ViewHolder(binding.root) { - - fun setUpWithNetworkState(state: LoadState) { - binding.progressBar.visible(state == LoadState.Loading) - binding.retryButton.visible(state is LoadState.Error) - val msg = if (state is LoadState.Error) { - state.error.message - } else { - null - } - binding.errorMsg.visible(msg != null) - binding.errorMsg.text = msg - binding.retryButton.setOnClickListener { - retryCallback() - } - } -} 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 0c9465142..a5a8ed27d 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 @@ -20,21 +20,40 @@ import android.view.ViewGroup import androidx.paging.PagingDataAdapter import androidx.recyclerview.widget.DiffUtil import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.util.StatusDisplayOptions class ConversationAdapter( - private val statusDisplayOptions: StatusDisplayOptions, + private var statusDisplayOptions: StatusDisplayOptions, private val listener: StatusActionListener ) : PagingDataAdapter(CONVERSATION_COMPARATOR) { + var mediaPreviewEnabled: Boolean + get() = statusDisplayOptions.mediaPreviewEnabled + set(mediaPreviewEnabled) { + statusDisplayOptions = statusDisplayOptions.copy( + mediaPreviewEnabled = mediaPreviewEnabled + ) + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false) return ConversationViewHolder(view, statusDisplayOptions, listener) } override fun onBindViewHolder(holder: ConversationViewHolder, position: Int) { - holder.setupWithConversation(getItem(position)) + onBindViewHolder(holder, position, emptyList()) + } + + override fun onBindViewHolder( + holder: ConversationViewHolder, + position: Int, + payloads: List + ) { + getItem(position)?.let { conversationViewData -> + holder.setupWithConversation(conversationViewData, payloads.firstOrNull()) + } } companion object { @@ -44,7 +63,17 @@ class ConversationAdapter( } override fun areContentsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean { - return oldItem == newItem + return false // Items are different always. It allows to refresh timestamp on every view holder update + } + + override fun getChangePayload(oldItem: ConversationViewData, newItem: ConversationViewData): Any? { + return if (oldItem == newItem) { + // If items are equal - update timestamp only + listOf(StatusBaseViewHolder.Key.KEY_CREATED) + } else { + // If items are different - update the whole view holder + null + } } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt index 5462ea7b5..401d61463 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 @@ -34,6 +34,7 @@ import java.util.Date data class ConversationEntity( val accountId: Long, val id: String, + val order: Int, val accounts: List, val unread: Boolean, @Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity @@ -41,6 +42,7 @@ data class ConversationEntity( fun toViewData(): ConversationViewData { return ConversationViewData( id = id, + order = order, accounts = accounts, unread = unread, lastStatus = lastStatus.toViewData() @@ -50,6 +52,7 @@ data class ConversationEntity( data class ConversationAccountEntity( val id: String, + val localUsername: String, val username: String, val displayName: String, val avatar: String, @@ -58,12 +61,12 @@ data class ConversationAccountEntity( fun toAccount(): TimelineAccount { return TimelineAccount( id = id, + localUsername = localUsername, username = username, displayName = displayName, url = "", avatar = avatar, emojis = emojis, - localUsername = "", ) } } @@ -134,6 +137,7 @@ data class ConversationStatusEntity( fun TimelineAccount.toEntity() = ConversationAccountEntity( id = id, + localUsername = localUsername, username = username, displayName = name, avatar = avatar, @@ -166,10 +170,11 @@ fun Status.toEntity() = poll = poll ) -fun Conversation.toEntity(accountId: Long) = +fun Conversation.toEntity(accountId: Long, order: Int) = ConversationEntity( accountId = accountId, id = id, + order = order, accounts = accounts.map { it.toEntity() }, unread = unread, lastStatus = lastStatus!!.toEntity() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt index c7224c4d2..7ff4daa74 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt @@ -19,22 +19,35 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.paging.LoadState import androidx.paging.LoadStateAdapter -import com.keylesspalace.tusky.adapter.NetworkStateViewHolder import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.visible class ConversationLoadStateAdapter( private val retryCallback: () -> Unit -) : LoadStateAdapter() { +) : LoadStateAdapter>() { - override fun onBindViewHolder(holder: NetworkStateViewHolder, loadState: LoadState) { - holder.setUpWithNetworkState(loadState) + override fun onBindViewHolder(holder: BindingHolder, loadState: LoadState) { + val binding = holder.binding + binding.progressBar.visible(loadState == LoadState.Loading) + binding.retryButton.visible(loadState is LoadState.Error) + val msg = if (loadState is LoadState.Error) { + loadState.error.message + } else { + null + } + binding.errorMsg.visible(msg != null) + binding.errorMsg.text = msg + binding.retryButton.setOnClickListener { + retryCallback() + } } override fun onCreateViewHolder( parent: ViewGroup, loadState: LoadState - ): NetworkStateViewHolder { + ): BindingHolder { val binding = ItemNetworkStateBinding.inflate(LayoutInflater.from(parent.context), parent, false) - return NetworkStateViewHolder(binding, retryCallback) + return BindingHolder(binding) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt index d63fce6c1..fae55f0ba 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt @@ -20,6 +20,7 @@ import com.keylesspalace.tusky.viewdata.StatusViewData data class ConversationViewData( val id: String, + val order: Int, val accounts: List, val unread: Boolean, val lastStatus: StatusViewData.Concrete @@ -37,6 +38,7 @@ data class ConversationViewData( return ConversationEntity( accountId = accountId, id = id, + order = order, accounts = accounts, unread = unread, lastStatus = lastStatus.toConversationStatusEntity( 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 ffb88a942..19280441e 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 @@ -23,6 +23,8 @@ import android.widget.Button; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; @@ -43,12 +45,12 @@ public class ConversationViewHolder extends StatusBaseViewHolder { private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE}; private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0]; - private TextView conversationNameTextView; - private Button contentCollapseButton; - private ImageView[] avatars; + private final TextView conversationNameTextView; + private final Button contentCollapseButton; + private final ImageView[] avatars; - private StatusDisplayOptions statusDisplayOptions; - private StatusActionListener listener; + private final StatusDisplayOptions statusDisplayOptions; + private final StatusActionListener listener; ConversationViewHolder(View itemView, StatusDisplayOptions statusDisplayOptions, @@ -64,7 +66,6 @@ public class ConversationViewHolder extends StatusBaseViewHolder { this.statusDisplayOptions = statusDisplayOptions; this.listener = listener; - } @Override @@ -72,52 +73,67 @@ public class ConversationViewHolder extends StatusBaseViewHolder { return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height); } - void setupWithConversation(ConversationViewData conversation) { + void setupWithConversation( + @NonNull ConversationViewData conversation, + @Nullable Object payloads + ) { + StatusViewData.Concrete statusViewData = conversation.getLastStatus(); Status status = statusViewData.getStatus(); - TimelineAccount account = status.getAccount(); - setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), statusViewData.getSpoilerText(), listener); + if (payloads == null) { + TimelineAccount account = status.getAccount(); - setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions); - setUsername(account.getUsername()); - setCreatedAt(status.getCreatedAt(), statusDisplayOptions); - setIsReply(status.getInReplyToId() != null); - setFavourited(status.getFavourited()); - setBookmarked(status.getBookmarked()); - List attachments = status.getAttachments(); - boolean sensitive = status.getSensitive(); - if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { - setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(), - statusDisplayOptions.useBlurhash()); + setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), statusViewData.getSpoilerText(), listener); - if (attachments.size() == 0) { + setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions); + setUsername(account.getUsername()); + setCreatedAt(status.getCreatedAt(), statusDisplayOptions); + setIsReply(status.getInReplyToId() != null); + setFavourited(status.getFavourited()); + setBookmarked(status.getBookmarked()); + List attachments = status.getAttachments(); + boolean sensitive = status.getSensitive(); + if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { + setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(), + statusDisplayOptions.useBlurhash()); + + if (attachments.size() == 0) { + hideSensitiveMediaWarning(); + } + // Hide the unused label. + for (TextView mediaLabel : mediaLabels) { + mediaLabel.setVisibility(View.GONE); + } + } else { + setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent()); + // Hide all unused views. + mediaPreviews[0].setVisibility(View.GONE); + mediaPreviews[1].setVisibility(View.GONE); + mediaPreviews[2].setVisibility(View.GONE); + mediaPreviews[3].setVisibility(View.GONE); hideSensitiveMediaWarning(); } - // Hide the unused label. - for (TextView mediaLabel : mediaLabels) { - mediaLabel.setVisibility(View.GONE); - } + + setupButtons(listener, account.getId(), statusViewData.getContent().toString(), + statusDisplayOptions); + + setSpoilerAndContent(statusViewData.isExpanded(), statusViewData.getContent(), status.getSpoilerText(), + status.getMentions(), status.getTags(), status.getEmojis(), + PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener); + + setConversationName(conversation.getAccounts()); + + setAvatars(conversation.getAccounts()); } else { - setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent()); - // Hide all unused views. - mediaPreviews[0].setVisibility(View.GONE); - mediaPreviews[1].setVisibility(View.GONE); - mediaPreviews[2].setVisibility(View.GONE); - mediaPreviews[3].setVisibility(View.GONE); - hideSensitiveMediaWarning(); + if (payloads instanceof List) { + for (Object item : (List) payloads) { + if (Key.KEY_CREATED.equals(item)) { + setCreatedAt(status.getCreatedAt(), statusDisplayOptions); + } + } + } } - - setupButtons(listener, account.getId(), statusViewData.getContent().toString(), - statusDisplayOptions); - - setSpoilerAndContent(statusViewData.isExpanded(), statusViewData.getContent(), status.getSpoilerText(), - status.getMentions(), status.getTags(), status.getEmojis(), - PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener); - - setConversationName(conversation.getAccounts()); - - setAvatars(conversation.getAccounts()); } private void setConversationName(List accounts) { @@ -169,4 +185,4 @@ public class ConversationViewHolder extends StatusBaseViewHolder { content.setFilters(NO_INPUT_FILTER); } } -} \ No newline at end of file +} 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 243c37448..b05df2f8b 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 @@ -22,20 +22,27 @@ import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.PopupMenu import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope -import androidx.paging.ExperimentalPagingApi import androidx.paging.LoadState import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator +import at.connyduck.sparkbutton.helpers.Utils +import autodispose2.androidx.lifecycle.autoDispose import com.keylesspalace.tusky.R import com.keylesspalace.tusky.StatusListActivity +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent import com.keylesspalace.tusky.components.account.AccountActivity import com.keylesspalace.tusky.databinding.FragmentTimelineBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.fragment.SFragment +import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.settings.PrefKeys @@ -44,29 +51,31 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding -import com.keylesspalace.tusky.util.visible import com.keylesspalace.tusky.viewdata.AttachmentViewData +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import java.io.IOException import javax.inject.Inject +import kotlin.time.DurationUnit +import kotlin.time.toDuration -@OptIn(ExperimentalPagingApi::class) class ConversationsFragment : SFragment(), StatusActionListener, Injectable, ReselectableFragment { @Inject lateinit var viewModelFactory: ViewModelFactory + @Inject + lateinit var eventHub: EventHub + private val viewModel: ConversationsViewModel by viewModels { viewModelFactory } private val binding by viewBinding(FragmentTimelineBinding::bind) private lateinit var adapter: ConversationAdapter - private lateinit var loadStateAdapter: ConversationLoadStateAdapter - private var layoutManager: LinearLayoutManager? = null - - private var initialRefreshDone: Boolean = false + private var hideFab = false override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { return inflater.inflate(R.layout.fragment_timeline, container, false) @@ -89,56 +98,106 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res ) adapter = ConversationAdapter(statusDisplayOptions, this) - loadStateAdapter = ConversationLoadStateAdapter(adapter::retry) - binding.recyclerView.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL)) - layoutManager = LinearLayoutManager(view.context) - binding.recyclerView.layoutManager = layoutManager - binding.recyclerView.adapter = adapter.withLoadStateFooter(loadStateAdapter) - (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false - - binding.progressBar.hide() - binding.statusView.hide() + setupRecyclerView() initSwipeToRefresh() + adapter.addLoadStateListener { loadState -> + if (loadState.refresh != LoadState.Loading && loadState.source.refresh != LoadState.Loading) { + binding.swipeRefreshLayout.isRefreshing = false + } + + binding.statusView.hide() + binding.progressBar.hide() + + if (adapter.itemCount == 0) { + when (loadState.refresh) { + is LoadState.NotLoading -> { + if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) { + binding.statusView.show() + binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null) + } + } + is LoadState.Error -> { + binding.statusView.show() + + if ((loadState.refresh as LoadState.Error).error is IOException) { + binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network, null) + } else { + binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic, null) + } + } + is LoadState.Loading -> { + binding.progressBar.show() + } + } + } + } + + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + if (positionStart == 0 && adapter.itemCount != itemCount) { + binding.recyclerView.post { + if (getView() != null) { + binding.recyclerView.scrollBy(0, Utils.dpToPx(requireContext(), -30)) + } + } + } + } + }) + + hideFab = preferences.getBoolean(PrefKeys.FAB_HIDE, false) + binding.recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { + val composeButton = (activity as ActionButtonActivity).actionButton + if (composeButton != null) { + if (hideFab) { + if (dy > 0 && composeButton.isShown) { + composeButton.hide() // hides the button if we're scrolling down + } else if (dy < 0 && !composeButton.isShown) { + composeButton.show() // shows it if we are scrolling up + } + } else if (!composeButton.isShown) { + composeButton.show() + } + } + } + }) + viewLifecycleOwner.lifecycleScope.launch { viewModel.conversationFlow.collectLatest { pagingData -> adapter.submitData(pagingData) } } - adapter.addLoadStateListener { loadStates -> - - loadStates.refresh.let { refreshState -> - if (refreshState is LoadState.Error) { - binding.statusView.show() - if (refreshState.error is IOException) { - binding.statusView.setup(R.drawable.elephant_offline, R.string.error_network) { - adapter.refresh() - } - } else { - binding.statusView.setup(R.drawable.elephant_error, R.string.error_generic) { - adapter.refresh() - } - } - } else { - binding.statusView.hide() - } - - binding.progressBar.visible(refreshState == LoadState.Loading && adapter.itemCount == 0) - - if (refreshState is LoadState.NotLoading && !initialRefreshDone) { - // jump to top after the initial refresh finished - binding.recyclerView.scrollToPosition(0) - initialRefreshDone = true - } - - if (refreshState != LoadState.Loading) { - binding.swipeRefreshLayout.isRefreshing = false - } + lifecycleScope.launchWhenResumed { + val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false) + while (!useAbsoluteTime) { + adapter.notifyItemRangeChanged(0, adapter.itemCount, listOf(StatusBaseViewHolder.Key.KEY_CREATED)) + delay(1.toDuration(DurationUnit.MINUTES)) } } + + eventHub.events + .observeOn(AndroidSchedulers.mainThread()) + .autoDispose(this, Lifecycle.Event.ON_DESTROY) + .subscribe { event -> + if (event is PreferenceChangedEvent) { + onPreferenceChanged(event.preferenceKey) + } + } + } + + private fun setupRecyclerView() { + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(context) + + binding.recyclerView.addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL)) + + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + + binding.recyclerView.adapter = adapter.withLoadStateFooter(ConversationLoadStateAdapter(adapter::retry)) } private fun initSwipeToRefresh() { @@ -201,7 +260,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onOpenReblog(position: Int) { - // there are no reblogs in search results + // there are no reblogs in conversations } override fun onExpandedChange(expanded: Boolean, position: Int) { @@ -246,6 +305,19 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } } + override fun onVoteInPoll(position: Int, choices: MutableList) { + adapter.peek(position)?.let { conversation -> + viewModel.voteInPoll(choices, conversation) + } + } + + override fun onReselect() { + if (isAdded) { + binding.recyclerView.layoutManager?.scrollToPosition(0) + binding.recyclerView.stopScroll() + } + } + private fun deleteConversation(conversation: ConversationViewData) { AlertDialog.Builder(requireContext()) .setMessage(R.string.dialog_delete_conversation_warning) @@ -256,20 +328,20 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res .show() } - private fun jumpToTop() { - if (isAdded) { - layoutManager?.scrollToPosition(0) - binding.recyclerView.stopScroll() - } - } - - override fun onReselect() { - jumpToTop() - } - - override fun onVoteInPoll(position: Int, choices: MutableList) { - adapter.peek(position)?.let { conversation -> - viewModel.voteInPoll(choices, conversation) + private fun onPreferenceChanged(key: String) { + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + when (key) { + PrefKeys.FAB_HIDE -> { + hideFab = sharedPreferences.getBoolean(PrefKeys.FAB_HIDE, false) + } + PrefKeys.MEDIA_PREVIEW_ENABLED -> { + val enabled = accountManager.activeAccount!!.mediaPreviewEnabled + val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled + if (enabled != oldMediaPreviewEnabled) { + adapter.mediaPreviewEnabled = enabled + adapter.notifyItemRangeChanged(0, adapter.itemCount) + } + } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt index 26984c8e8..02a44f951 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt @@ -4,8 +4,11 @@ import androidx.paging.ExperimentalPagingApi import androidx.paging.LoadType import androidx.paging.PagingState import androidx.paging.RemoteMediator +import androidx.room.withTransaction import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.HttpHeaderLink +import retrofit2.HttpException @OptIn(ExperimentalPagingApi::class) class ConversationsRemoteMediator( @@ -14,38 +17,53 @@ class ConversationsRemoteMediator( private val db: AppDatabase ) : RemoteMediator() { + private var nextKey: String? = null + + private var order: Int = 0 + override suspend fun load( loadType: LoadType, state: PagingState ): MediatorResult { + if (loadType == LoadType.PREPEND) { + return MediatorResult.Success(endOfPaginationReached = true) + } + + if (loadType == LoadType.REFRESH) { + nextKey = null + order = 0 + } + try { - val conversationsResult = when (loadType) { - LoadType.REFRESH -> { - api.getConversations(limit = state.config.initialLoadSize) - } - LoadType.PREPEND -> { - return MediatorResult.Success(endOfPaginationReached = true) - } - LoadType.APPEND -> { - val maxId = state.pages.findLast { it.data.isNotEmpty() }?.data?.lastOrNull()?.lastStatus?.id - api.getConversations(maxId = maxId, limit = state.config.pageSize) - } + val conversationsResponse = api.getConversations(maxId = nextKey, limit = state.config.pageSize) + + val conversations = conversationsResponse.body() + if (!conversationsResponse.isSuccessful || conversations == null) { + return MediatorResult.Error(HttpException(conversationsResponse)) } - if (loadType == LoadType.REFRESH) { - db.conversationDao().deleteForAccount(accountId) + db.withTransaction { + + if (loadType == LoadType.REFRESH) { + db.conversationDao().deleteForAccount(accountId) + } + + val linkHeader = conversationsResponse.headers()["Link"] + val links = HttpHeaderLink.parse(linkHeader) + nextKey = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter("max_id") + + db.conversationDao().insert( + conversations + .filterNot { it.lastStatus == null } + .map { + it.toEntity(accountId, order++) + } + ) } - db.conversationDao().insert( - conversationsResult - .filterNot { it.lastStatus == null } - .map { it.toEntity(accountId) } - ) - return MediatorResult.Success(endOfPaginationReached = conversationsResult.isEmpty()) + return MediatorResult.Success(endOfPaginationReached = nextKey == null) } catch (e: Exception) { return MediatorResult.Error(e) } } - - override suspend fun initialize() = InitializeAction.LAUNCH_INITIAL_REFRESH } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt index 12c5eb0bb..3f074be5b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt @@ -16,22 +16,15 @@ package com.keylesspalace.tusky.components.conversation import com.keylesspalace.tusky.db.AppDatabase -import com.keylesspalace.tusky.network.MastodonApi -import io.reactivex.rxjava3.core.Single -import io.reactivex.rxjava3.schedulers.Schedulers import javax.inject.Inject import javax.inject.Singleton @Singleton class ConversationsRepository @Inject constructor( - val mastodonApi: MastodonApi, val db: AppDatabase ) { - fun deleteCacheForAccount(accountId: Long) { - Single.fromCallable { - db.conversationDao().deleteForAccount(accountId) - }.subscribeOn(Schedulers.io()) - .subscribe() + suspend fun deleteCacheForAccount(accountId: Long) { + db.conversationDao().deleteForAccount(accountId) } } 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 9326a05c0..684c6f011 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 @@ -41,7 +41,7 @@ class ConversationsViewModel @Inject constructor( @OptIn(ExperimentalPagingApi::class) val conversationFlow = Pager( - config = PagingConfig(pageSize = 10, enablePlaceholders = false, initialLoadSize = 20), + config = PagingConfig(pageSize = 30), remoteMediator = ConversationsRemoteMediator(accountManager.activeAccount!!.id, api, database), pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) } ) diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index bdbfb09cc..5bce53340 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -565,10 +565,12 @@ public abstract class AppDatabase extends RoomDatabase { public static final Migration MIGRATION_37_38 = new Migration(37, 38) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { - - // no actual scheme change, but timestamps are now serialized differently so all cache tables that contain them need to be cleaned - database.execSQL("DELETE FROM `TimelineStatusEntity`"); + // database needs to be cleaned because the ConversationAccountEntity got a new attribute database.execSQL("DELETE FROM `ConversationEntity`"); + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `order` INTEGER NOT NULL DEFAULT 0"); + + // timestamps are now serialized differently so all cache tables that contain them need to be cleaned + database.execSQL("DELETE FROM `TimelineStatusEntity`"); } }; } 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 fe093bd0c..001dbbe5e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt @@ -28,14 +28,14 @@ interface ConversationsDao { suspend fun insert(conversations: List) @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun insert(conversation: ConversationEntity): Long + suspend fun insert(conversation: ConversationEntity) @Query("DELETE FROM ConversationEntity WHERE id = :id AND accountId = :accountId") - suspend fun delete(id: String, accountId: Long): Int + suspend fun delete(id: String, accountId: Long) - @Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC") + @Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY `order` ASC") fun conversationsForAccount(accountId: Long): PagingSource @Query("DELETE FROM ConversationEntity WHERE accountId = :accountId") - fun deleteForAccount(accountId: Long) + suspend fun deleteForAccount(accountId: Long) } 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 0d9a1945c..abd3c1244 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -503,8 +503,8 @@ interface MastodonApi { @GET("/api/v1/conversations") suspend fun getConversations( @Query("max_id") maxId: String? = null, - @Query("limit") limit: Int - ): List + @Query("limit") limit: Int? = null + ): Response> @DELETE("/api/v1/conversations/{id}") suspend fun deleteConversation( From e1c8461423ccc14009826d65a4df6306417fd577 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 30 May 2022 20:03:40 +0200 Subject: [PATCH 29/82] replace kotlin-result-calladapter with networkresult-calladapter (#2569) * replace kotlin-result-calladapter with networkresult-calladapter * fix tests --- app/build.gradle | 2 +- .../com/keylesspalace/tusky/MainActivity.kt | 1 + .../tusky/TabPreferenceActivity.kt | 1 + .../announcements/AnnouncementsViewModel.kt | 1 + .../components/compose/ComposeViewModel.kt | 8 +-- .../instanceinfo/InstanceInfoRepository.kt | 3 + .../tusky/components/login/LoginActivity.kt | 1 + .../notifications/PushNotificationHelper.kt | 2 + .../scheduled/ScheduledStatusViewModel.kt | 1 + .../keylesspalace/tusky/di/NetworkModule.kt | 4 +- .../tusky/network/MastodonApi.kt | 57 ++++++++++--------- .../tusky/service/SendStatusService.kt | 1 + .../tusky/util/CallExtensions.kt | 23 -------- .../viewmodel/AccountsInListViewModel.kt | 1 + .../tusky/viewmodel/EditProfileViewModel.kt | 1 + .../tusky/viewmodel/ListsViewModel.kt | 1 + .../tusky/ComposeActivityTest.kt | 7 ++- 17 files changed, 53 insertions(+), 62 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/util/CallExtensions.kt diff --git a/app/build.gradle b/app/build.gradle index 54e3452dc..a58748a5a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -141,7 +141,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.1" + implementation "at.connyduck:networkresult-calladapter:1.0.0" implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" implementation "com.squareup.okhttp3:logging-interceptor:$okhttpVersion" diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 51475c411..4fe8df6da 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -40,6 +40,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager import androidx.viewpager2.widget.MarginPageTransformer +import at.connyduck.calladapter.networkresult.fold import autodispose2.androidx.lifecycle.autoDispose import com.bumptech.glide.Glide import com.bumptech.glide.RequestManager diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt index b900e7564..76418e019 100644 --- a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt @@ -31,6 +31,7 @@ import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.transition.TransitionManager +import at.connyduck.calladapter.networkresult.fold import at.connyduck.sparkbutton.helpers.Utils import autodispose2.androidx.lifecycle.AndroidLifecycleScopeProvider.from import autodispose2.autoDispose 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 0934c48fc..c7e6781a1 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 @@ -20,6 +20,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.appstore.AnnouncementReadEvent import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository 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 b7726c38e..f15565d03 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 @@ -23,6 +23,7 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter.AutocompleteResult import com.keylesspalace.tusky.components.drafts.DraftHelper @@ -39,7 +40,6 @@ import com.keylesspalace.tusky.service.ServiceClient import com.keylesspalace.tusky.service.StatusToSend import com.keylesspalace.tusky.util.combineLiveData import com.keylesspalace.tusky.util.randomAlphanumericString -import com.keylesspalace.tusky.util.result import com.keylesspalace.tusky.util.toLiveData import io.reactivex.rxjava3.core.Observable import kotlinx.coroutines.Dispatchers @@ -351,8 +351,7 @@ class ComposeViewModel @Inject constructor( fun searchAutocompleteSuggestions(token: String): List { when (token[0]) { '@' -> { - return api.searchAccountsCall(query = token.substring(1), limit = 10) - .result() + return api.searchAccountsSync(query = token.substring(1), limit = 10) .fold({ accounts -> accounts.map { AutocompleteResult.AccountResult(it) } }, { e -> @@ -361,8 +360,7 @@ class ComposeViewModel @Inject constructor( }) } '#' -> { - return api.searchCall(query = token, type = SearchType.Hashtag.apiParameter, limit = 10) - .result() + return api.searchSync(query = token, type = SearchType.Hashtag.apiParameter, limit = 10) .fold({ searchResult -> searchResult.hashtags.map { AutocompleteResult.HashtagResult(it.name) } }, { e -> 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 8ed26d7b1..20c44ba47 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 @@ -16,6 +16,9 @@ package com.keylesspalace.tusky.components.instanceinfo import android.util.Log +import at.connyduck.calladapter.networkresult.fold +import at.connyduck.calladapter.networkresult.getOrElse +import at.connyduck.calladapter.networkresult.onSuccess import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.EmojisEntity 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 8066482eb..4b82340b2 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 @@ -26,6 +26,7 @@ import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.core.net.toUri import androidx.lifecycle.lifecycleScope +import at.connyduck.calladapter.networkresult.fold import com.bumptech.glide.Glide import com.keylesspalace.tusky.BaseActivity import com.keylesspalace.tusky.BuildConfig diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt index ec2c82ac9..a819b282c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt @@ -23,6 +23,8 @@ import android.util.Log import android.view.View import androidx.appcompat.app.AlertDialog import androidx.preference.PreferenceManager +import at.connyduck.calladapter.networkresult.onFailure +import at.connyduck.calladapter.networkresult.onSuccess import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.login.LoginActivity 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 766ed44ab..483c8e4df 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 @@ -21,6 +21,7 @@ import androidx.lifecycle.viewModelScope import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn +import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.entity.ScheduledStatus import com.keylesspalace.tusky.network.MastodonApi 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 c49fbd132..61c4ac88b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -18,7 +18,7 @@ package com.keylesspalace.tusky.di import android.content.Context import android.content.SharedPreferences import android.os.Build -import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory +import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory import com.google.gson.Gson import com.google.gson.GsonBuilder import com.keylesspalace.tusky.BuildConfig @@ -111,7 +111,7 @@ class NetworkModule { .client(httpClient) .addConverterFactory(GsonConverterFactory.create(gson)) .addCallAdapterFactory(RxJava3CallAdapterFactory.create()) - .addCallAdapterFactory(KotlinResultCallAdapterFactory.create()) + .addCallAdapterFactory(NetworkResultCallAdapterFactory.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 abd3c1244..12d7cdcb4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -15,6 +15,7 @@ package com.keylesspalace.tusky.network +import at.connyduck.calladapter.networkresult.NetworkResult import com.keylesspalace.tusky.entity.AccessToken import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Announcement @@ -74,10 +75,10 @@ interface MastodonApi { } @GET("/api/v1/custom_emojis") - suspend fun getCustomEmojis(): Result> + suspend fun getCustomEmojis(): NetworkResult> @GET("api/v1/instance") - suspend fun getInstance(): Result + suspend fun getInstance(): NetworkResult @GET("api/v1/filters") fun getFilters(): Single> @@ -145,7 +146,7 @@ interface MastodonApi { suspend fun updateMedia( @Path("mediaId") mediaId: String, @Field("description") description: String - ): Result + ): NetworkResult @GET("api/v1/media/{mediaId}") suspend fun getMedia( @@ -158,7 +159,7 @@ interface MastodonApi { @Header(DOMAIN_HEADER) domain: String, @Header("Idempotency-Key") idempotencyKey: String, @Body status: NewStatus - ): Result + ): NetworkResult @GET("api/v1/statuses/{id}") fun status( @@ -246,10 +247,10 @@ interface MastodonApi { @DELETE("api/v1/scheduled_statuses/{id}") suspend fun deleteScheduledStatus( @Path("id") scheduledStatusId: String - ): Result + ): NetworkResult @GET("api/v1/accounts/verify_credentials") - suspend fun accountVerifyCredentials(): Result + suspend fun accountVerifyCredentials(): NetworkResult @FormUrlEncoded @PATCH("api/v1/accounts/update_credentials") @@ -274,7 +275,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? - ): Result + ): NetworkResult @GET("api/v1/accounts/search") suspend fun searchAccounts( @@ -282,15 +283,15 @@ interface MastodonApi { @Query("resolve") resolve: Boolean? = null, @Query("limit") limit: Int? = null, @Query("following") following: Boolean? = null - ): Result> + ): NetworkResult> @GET("api/v1/accounts/search") - fun searchAccountsCall( + fun searchAccountsSync( @Query("q") query: String, @Query("resolve") resolve: Boolean? = null, @Query("limit") limit: Int? = null, @Query("following") following: Boolean? = null - ): Call> + ): NetworkResult> @GET("api/v1/accounts/{id}") fun account( @@ -445,7 +446,7 @@ interface MastodonApi { @Field("redirect_uris") redirectUris: String, @Field("scopes") scopes: String, @Field("website") website: String - ): Result + ): NetworkResult @FormUrlEncoded @POST("oauth/token") @@ -456,34 +457,34 @@ interface MastodonApi { @Field("redirect_uri") redirectUri: String, @Field("code") code: String, @Field("grant_type") grantType: String - ): Result + ): NetworkResult @GET("/api/v1/lists") - suspend fun getLists(): Result> + suspend fun getLists(): NetworkResult> @FormUrlEncoded @POST("api/v1/lists") suspend fun createList( @Field("title") title: String - ): Result + ): NetworkResult @FormUrlEncoded @PUT("api/v1/lists/{listId}") suspend fun updateList( @Path("listId") listId: String, @Field("title") title: String - ): Result + ): NetworkResult @DELETE("api/v1/lists/{listId}") suspend fun deleteList( @Path("listId") listId: String - ): Result + ): NetworkResult @GET("api/v1/lists/{listId}/accounts") suspend fun getAccountsInList( @Path("listId") listId: String, @Query("limit") limit: Int - ): Result> + ): NetworkResult> @FormUrlEncoded // @DELETE doesn't support fields @@ -491,14 +492,14 @@ interface MastodonApi { suspend fun deleteAccountFromList( @Path("listId") listId: String, @Field("account_ids[]") accountIds: List - ): Result + ): NetworkResult @FormUrlEncoded @POST("api/v1/lists/{listId}/accounts") suspend fun addAccountToList( @Path("listId") listId: String, @Field("account_ids[]") accountIds: List - ): Result + ): NetworkResult @GET("/api/v1/conversations") suspend fun getConversations( @@ -547,24 +548,24 @@ interface MastodonApi { @GET("api/v1/announcements") suspend fun listAnnouncements( @Query("with_dismissed") withDismissed: Boolean = true - ): Result> + ): NetworkResult> @POST("api/v1/announcements/{id}/dismiss") suspend fun dismissAnnouncement( @Path("id") announcementId: String - ): Result + ): NetworkResult @PUT("api/v1/announcements/{id}/reactions/{name}") suspend fun addAnnouncementReaction( @Path("id") announcementId: String, @Path("name") name: String - ): Result + ): NetworkResult @DELETE("api/v1/announcements/{id}/reactions/{name}") suspend fun removeAnnouncementReaction( @Path("id") announcementId: String, @Path("name") name: String - ): Result + ): NetworkResult @FormUrlEncoded @POST("api/v1/reports") @@ -601,14 +602,14 @@ interface MastodonApi { ): Single @GET("api/v2/search") - fun searchCall( + fun searchSync( @Query("q") query: String?, @Query("type") type: String? = null, @Query("resolve") resolve: Boolean? = null, @Query("limit") limit: Int? = null, @Query("offset") offset: Int? = null, @Query("following") following: Boolean? = null - ): Call + ): NetworkResult @FormUrlEncoded @POST("api/v1/accounts/{id}/note") @@ -629,7 +630,7 @@ interface MastodonApi { // Should be generated dynamically from all the available notification // types defined in [com.keylesspalace.tusky.entities.Notification.Types] @FieldMap data: Map - ): Result + ): NetworkResult @FormUrlEncoded @PUT("api/v1/push/subscription") @@ -637,11 +638,11 @@ interface MastodonApi { @Header("Authorization") auth: String, @Header(DOMAIN_HEADER) domain: String, @FieldMap data: Map - ): Result + ): NetworkResult @DELETE("api/v1/push/subscription") suspend fun unsubscribePushNotifications( @Header("Authorization") auth: String, @Header(DOMAIN_HEADER) domain: String, - ): Result + ): NetworkResult } 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 e50f4f4f2..20ad8de8b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt @@ -15,6 +15,7 @@ import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat import androidx.core.content.ContextCompat +import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.R import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.StatusComposedEvent diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CallExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/CallExtensions.kt deleted file mode 100644 index 809dcd2b4..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/CallExtensions.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.keylesspalace.tusky.util - -import retrofit2.Call -import retrofit2.HttpException - -/** - * Synchronously executes the call and returns the response encapsulated in a kotlin.Result. - * Since Result is an inline class it is not possible to do this with a Retrofit adapter unfortunately. - * More efficient then calling a suspending method with runBlocking - */ -fun Call.result(): Result { - return try { - val response = execute() - val responseBody = response.body() - if (response.isSuccessful && responseBody != null) { - Result.success(responseBody) - } else { - Result.failure(HttpException(response)) - } - } catch (e: Exception) { - Result.failure(e) - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt index 184debb9b..aafe4ce05 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt @@ -19,6 +19,7 @@ package com.keylesspalace.tusky.viewmodel import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.entity.TimelineAccount import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.Either 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 17aa38c75..72835acc4 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -21,6 +21,7 @@ import androidx.core.net.toUri import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.ProfileEditedEvent import com.keylesspalace.tusky.entity.Account diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt index 3b5b824b1..4c755f868 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt @@ -18,6 +18,7 @@ package com.keylesspalace.tusky.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.entity.MastoList import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.replacedFirstWhich diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index 95d1bae0e..f58e33bd5 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -19,6 +19,7 @@ import android.content.Intent import android.os.Looper.getMainLooper import android.widget.EditText import androidx.test.ext.junit.runners.AndroidJUnit4 +import at.connyduck.calladapter.networkresult.NetworkResult import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository @@ -95,12 +96,12 @@ class ComposeActivityTest { } apiMock = mock { - onBlocking { getCustomEmojis() } doReturn Result.success(emptyList()) + onBlocking { getCustomEmojis() } doReturn NetworkResult.success(emptyList()) onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance -> if (instance == null) { - Result.failure(Throwable()) + NetworkResult.failure(Throwable()) } else { - Result.success(instance) + NetworkResult.success(instance) } } } From 530eb6150564cf5c8e3a28506202c03eaf3daf33 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Mon, 6 Jun 2022 16:04:33 +0200 Subject: [PATCH 30/82] Use NetworkResult in MediaUploadApi (#2577) --- .../keylesspalace/tusky/components/compose/MediaUploader.kt | 1 + .../java/com/keylesspalace/tusky/network/MediaUploadApi.kt | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) 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 b2915c799..de7141a15 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 @@ -23,6 +23,7 @@ import android.util.Log import android.webkit.MimeTypeMap import androidx.core.content.FileProvider import androidx.core.net.toUri +import at.connyduck.calladapter.networkresult.getOrThrow import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt index c7e9633f6..a179e71d2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt @@ -1,5 +1,6 @@ package com.keylesspalace.tusky.network +import at.connyduck.calladapter.networkresult.NetworkResult import com.keylesspalace.tusky.entity.MediaUploadResult import okhttp3.MultipartBody import retrofit2.http.Multipart @@ -15,5 +16,5 @@ interface MediaUploadApi { suspend fun uploadMedia( @Part file: MultipartBody.Part, @Part description: MultipartBody.Part? = null - ): Result + ): NetworkResult } From ac205d7700085604f26c0b5b60a594509a00c6f5 Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Mon, 30 May 2022 16:40:42 +0000 Subject: [PATCH 31/82] Translated using Weblate (Persian) Currently translated at 88.2% (15 of 17 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/fa/ --- fastlane/metadata/android/fa/changelogs/91.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 fastlane/metadata/android/fa/changelogs/91.txt diff --git a/fastlane/metadata/android/fa/changelogs/91.txt b/fastlane/metadata/android/fa/changelogs/91.txt new file mode 100644 index 000000000..71c5a0aca --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/91.txt @@ -0,0 +1,6 @@ +تاسکی نگارش ۱۸٫۰ + +- پشتیبانی از گونه‌های آگاهی جدید ماستودون ۳٫۵ +- نشان بات اکنون ظاهر بهتری داشته و با زمینهٔ گزیده تنظیم می‌شود +- متن‌ها اکنون می‌توانند در نمای جزییات فرسته، گزیده شوند +- رفع کلّی مشکل، از جمله مشکلی که جلوی ورود روی اندروید ۶ و پایین‌تر را می‌گرفت From 0fd43d0b547d781d0933fb29e1e165387fdc61e8 Mon Sep 17 00:00:00 2001 From: Danial Behzadi Date: Thu, 2 Jun 2022 09:40:43 +0000 Subject: [PATCH 32/82] Translated using Weblate (Persian) Currently translated at 100.0% (488 of 488 strings) Co-authored-by: Danial Behzadi Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/fa/ Translation: Tusky/Tusky --- app/src/main/res/values-fa/strings.xml | 115 ++++++++++++++++--------- 1 file changed, 73 insertions(+), 42 deletions(-) diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 90d643faa..49f3912ed 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -8,22 +8,22 @@ خطای احراز هویت ناشناخته‌ای رخ داد. احراز هویت رد شد. دریافت ژتون ورود شکست خورد. - وضعیت خیلی طولانی است! + فرسته خیلی طولانی است! پرونده باید کمتر از ۸ مگابایت باشد. پرونده ویدئویی باید کمتر از ۴۰ مگابایت باشد. این گونهٔ پرونده نمی‌تواند بارگذاری شود. این پرونده نتوانست گشوده شود. نیاز به اجازهٔ خواندن رسانه است. نیاز به اجازهٔ ذخیرهٔ رسانه است. - تصاویر و فیلم‌ها هر دو نمی‌توانند به یک وضعیت ضمیمه شوند. + تصاویر و فیلم‌ها نمی‌توانند به یک فرسته پیوست شوند. بارگذاری شکست خورد. - خطای فرستادن بوق. + خطای فرستادن فرسته. خانه آگاهی‌ها محلّی همگانی - بوق - فرسته + رشته + فرسته‌ها با پاسخ‌ دنبال شونده پی‌گیر @@ -41,10 +41,10 @@ نمایش بیش‌تر نمایش کم‌تر گسترش - بستن + جمع کردن این‌جا هیچ‌چیز نیست. برای تازه‌سازی، به پایین بکشید! - %s بوقتان را تقویت کرد - %s بوقتان را برگزید + %s فرسته‌تان را تقویت کرد + %s فرسته‌تان را برگزید %s پی‌گیرتان شد گزارش @%s نظرهای اضافی؟ @@ -93,13 +93,13 @@ رد جست‌وجو پیش‌نویس‌ها - نمایانی بوق + نمایانی فرسته هشدار محتوا صفحه‌کلید اموجی درحال بارگیری %1$s رونوشت از پیوند - هم‌رسانی نشانی بوق با… - هم‌رسانی بوق با… + هم‌رسانی نشانی فرسته با… + هم‌رسانی فرسته با… هم‌رسانی رسانه با… فرستاده شد! کاربرنامسدود شد @@ -130,7 +130,7 @@ بارگیری درخواست دنبال کردن را لغو می‌کنید؟ ناپیگیری این حساب؟ - حذف این بوق؟ + حذف این فرسته؟ عمومی: فرستادن به خط زمانی‌های عمومی فهرست‌نشده: نشان ندادن در خط زمانی‌های عمومی تنها دنبال‌کنندگان:پست فقط به دنبال‌کنندگان @@ -156,7 +156,7 @@ مرورگر استفاده از زبانه‌های سفارشی کروم نهفتن دکمهٔ ایجاد، هنگام پیمایش - فیلتر کردن خط زمانی + پالایش خط زمانی زبانه‌ها نمایش تقویت‌ها نمایش پاسخ‌ها @@ -173,10 +173,10 @@ عمومی فهرست‌نشده فقط پی‌گیران - اندازهٔ متن وضعیت + اندازهٔ متن فرسته کوچک‌ترین کوچک - متوسط + میانه بزرگ بزرگ‌ترین اشاره‌های جدید @@ -184,9 +184,9 @@ پی‌گیران جدید آگاهی‌ها دربارهٔ پی‌گیران جدید تقویت‌ها - آگاهی‌ها هنگام تقویت شدن بوق‌هایتان + آگاهی‌ها هنگام تقویت فرسته‌هایتان برگزیدن‌ها - آگاهی‌ها هنگام برگزیده شدن بوق‌هایتان + آگاهی‌ها هنگام برگزیده شدن فرسته‌هایتان %s به شما اشاره کرد %1$s، %2$s، %3$s و %4$d دیگر %1$s، %2$s و %3$s @@ -209,8 +209,8 @@ گزارش مشکلات و درخواست ویژگی‌ها: \n https://github.com/tuskyapp/Tusky/issues نمایهٔ تاسکی - هم‌رسانی محتوای بوق - هم‌رسانی پیوند بوق + هم‌رسانی محتوای فرسته + هم‌رسانی پیوند فرسته تصویرها ویدیو تقاضای پیگیری شد @@ -240,19 +240,19 @@ قفل حساب لازم است پی‌گیران را دستی تأیید کنید ذخیرهٔ پیش‌نویس؟ - در حال فرستادن بوق… - خطای فرستادن بوق - در حال فرستادن بوق‌ها + فرستادن فرسته… + خطا در فرستادن فرسته + فرستادن فرسته‌ها فرستادن لغو شد - رونوشتی از بوق در پیش‌نویس‌هایتان ذخیره شد + رونوشتی از فرسته در پیش‌نویس‌هایتان ذخیره شد ایجاد نمونه‌تان %s هیچ اموجی سفارشی‌ای ندارد سبک اموجی پیش‌گزیدهٔ سامانه نخست باید این مجموعه‌های اموجی را بارگیری کنید در حال جست‌وجو… - گسترده/جمع کردن تمام وضعیت‌ها - گشودن بوق + گسترش/جمع کردن تمام فرسته‌ها + گشودن فرسته نیاز به آغاز دوبارهٔ کاره برای اعمال این تغییرات، نیاز به شروع دوبارهٔ تاسکی دارید بعداً @@ -278,7 +278,7 @@ یک خطای شبکه رخ داد! لطفا اتصال خود را بررسی و دوباره تلاش کنید! پیام‌های مستقیم زبانه‌ها - سنجاق‌شده + سنجاق شده دامنه‌های نهفته \@%s این‌جا هیچ‌چیزی نیست. @@ -304,7 +304,7 @@ بارگیری رسانه در حال بارگیری رسانه %s نانهفته - می‌خواهید این بوق را پاک و بازنویسی کنید؟ + حذف و بازنویسی این فرسته؟ نهفتن تمام دامنه پایان نظرسنجی‌ها پالایه‌ها @@ -320,7 +320,7 @@ %d ساعت %d دقیقه %d ثانیه - گسترش همیشگی بوق‌های علامت‌خورده با هشدار محتوا + گسترش همیشگی فرسته‌های علامت‌خورده با هشدار محتوا خط زمانی‌های عمومی گفت‌وگوها افزودن پالایه @@ -360,8 +360,8 @@ رسانه: %s هشدار محتوا: %s - بدون هیچ توضیحی - بازبوقیده + بدون شرح + تقویت شده برگزیده عمومی فهرست‌نشده @@ -373,7 +373,7 @@ پاک‌سازی پالایش اعمال - ایجاد بوق + ایجاد فرسته ایجاد مطمئنید می‌خواهید تمام آگاهی‌هایتان را برای همیشه پاک کنید؟ پایان در %s @@ -400,7 +400,7 @@ نظرهای اضافی هدایت به %s شکست در گزارش - شکست در واکشی وضعیت‌ها + شکست در واکشی فرسته‌ها حساب‌ها شکست در جست‌وجو نمایش پالایهٔ آگاهی‌ها @@ -416,10 +416,10 @@ گزینه‌های چندگانه گزینهٔ %d ویرایش - بوق‌های زمان‌بسته + فرسته‌های زمان‌بسته ویرایش - بوق‌های زمان‌بسته - بوق زمان‌بسته + فرسته‌های زمان‌بسته + فرستهٔ زمان‌بسته بازنشانی مطمئنید می‌خواهید تمام %s را مسدود کنید؟ محتوای آن دامنه را در هیچ‌یک از خط زمانی‌ها یا در آگاهی‌هایتان نخواهید دید. پی‌گیرانتان از آن دامنه، برداشته خواهند شد. هنگامی که کلیدواژه یا عبارت، فقط حروف‌عددی باشد، فقط اگر با تمام واژه مطابق باشد، اعمال خواهد شد @@ -437,7 +437,7 @@ گزینش فهرست فهرست هیچ پیش‌نویسی ندارید. - هیچ وضعیت زمان‌بسته‌ای ندارید. + هیچ فرستهٔ زمان‌بسته‌ای ندارید. ماستودون، بازهٔ زمان‌بندی‌ای با کمینهٔ ۵ دقیقه دارد. نمایش گفت‌وگوی تأیید، پیش از تقویت پیش‌نمایش پیوندها در خط‌زمانی‌ها @@ -482,7 +482,7 @@ عدم اشتراک اشتراک پیش‌نویس حذف شد - فرستادن این بوق شکست خورد! + فرستادن این فرسته شکست خورد! نهفتن آمار کمی روی نمایه‌ها نهفتن آمار کمی روی فرسته‌ها محدود کردن آگاهی‌های خط‌زمانی @@ -491,17 +491,17 @@ طول پیوست‌ها صدا - آگاهی‌ها هنگام انتشار بوقی جدید از کسی که مشترکش هستید - بوق‌های جدید + آگاهی‌ها هنگام انتشار فرسته‌ای جدید از کسی که پی‌می‌گیرید + فرسته‌های جدید اموجی‌های شخصی متحرّک - کسی که مشترکش شده‌ام، بوقی جدید منتشر کرد + کسی که پی‌می‌گیرم، فرسته‌ای جدید منتشر کرد %s چیزی فرستاد - بوقی که پاسخی به آن را پیش‌نویس کردید، برداشته شده + فرسته‌ای که پاسخی به آن را پیش‌نویس کردید، برداشته شده شکست در بار کردن اطّلاعات پاسخ برخی اطّلاعات که ممکن است روی سلامتی ذهنیتان تأثیر بگذارد، پنهان خواهند شد. همچون: \n \n - آگاهی‌های برگزیدن، تقویت و پی‌گیری -\n - شمار برگزیدن و تقویت بوق‌ها +\n - شمار برگزیدن و تقویت فرسته‌ها \n - آمار پی‌گیر و فرسته روی نمایه‌ها \n \n فرستادن آگاهی‌ها تأثیر نمی‌پذیرد، ولی می‌توانید ترجیحات آگاهیتان را به صورت دستی بازبینی کنید. @@ -514,4 +514,35 @@ با این که حسابتان قفل نیست، کارکنان %1$s فکر کردند ممکن است بخواهید درخواست‌های پی‌گیری از این حساب‌ها را دستی بازبینی کنید. حذف این گفت‌وگو؟ حذف گفت‌وگو + در %1$s پیوست + ورود + %s ثبت‌نام کرد + نمایش گفت‌وگوی تأیید پیش از برگزیدن + ایجاد فرسته + ورود دوباره به تمامی حساب‌ها برای به کار انداختن پشتیبانی آگاهی‌های ارسالی. + آگاهی‌ها هنگام ویرایش فرسته‌هایی که با آن‌ها تعامل داشته‌اید + برداشن نشانک + برای اعطای اجازهٔ اشتراک آگاهی‌های ارسالی ، دوباره به حسابتان وارد شدید. با این حال هنوز حساب‌هایی دیگر دارید که این‌گونه مهاجرت داده نشده‌اند. به آن‌ها رفته و برای به کار انداختن پشتیبانی آگاهی‌های UnifiedPush یکی‌یکی دوباره وارد شوید. + مطمئنید که می‌خواهید از حساب %1$s خارج شوید؟ + ۱۴ روز + ۳۰ روز + ۶۰ روز + ۹۰ روز + ۳۶۵ روز + ۱۸۰ روز + ۱+ + تاسکی برای استفاده از آگاهی‌های ارسالی با UnifiedPush نیاز به اجازهٔ اشتراک آگاهی‌ها روی کارساز ماستودنتان دارد. این کار نیازمند ورود دوباره برای تغییر حوزه‌های OAuth اعطایی به تاسکی است. استفاده از گزینهٔ ورود دوباره در این‌جا یا در ترجیحات حساب، تمامی انباره‌ها و پیش‌نویس‌های محلیتان را نگه خواهد داشت. + نتوانست صفحهٔ ورود را بار کند. + کسی ثبت‌نام کرد + پیوست نتوانست ویرایش شود. + ویرایش‌های فرسته + ویرایش تصویر + %s فرسته‌اش را ویراست + فرسته‌ای که با آن تعامل داشته‌ام ویرایش شده + ثبت‌نام‌ها + آگاهی‌ها دربارهٔ کاربران جدید + ورود دوباره برای آگاهی‌های ارسالی + رد کردن + جزییات + ذخیرهٔ پیش‌نویس… \ No newline at end of file From e378e03fae6db1fbb3211ec691960ba9b04fa281 Mon Sep 17 00:00:00 2001 From: hebbeff Date: Thu, 2 Jun 2022 09:40:43 +0000 Subject: [PATCH 33/82] Translated using Weblate (Chinese (zh_SG)) Currently translated at 69.2% (338 of 488 strings) Translated using Weblate (Chinese (Traditional)) Currently translated at 90.3% (441 of 488 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (488 of 488 strings) Co-authored-by: hebbeff Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/ Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hant/ Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_SG/ Translation: Tusky/Tusky --- app/src/main/res/values-zh-rCN/strings.xml | 16 ++++++------- app/src/main/res/values-zh-rSG/strings.xml | 26 ++++++++++------------ app/src/main/res/values-zh-rTW/strings.xml | 26 ++++++++++------------ 3 files changed, 32 insertions(+), 36 deletions(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 7427331cf..74054078e 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -112,7 +112,7 @@ 话题 打开转嘟用户主页 显示转嘟 - 显示收藏 + 显示喜欢 话题 提及 链接 @@ -337,7 +337,7 @@ 取消置顶 置顶 - <b>%1$s</b> 次收藏 + %1$s 次喜欢 <b>%s</b> 次转嘟 @@ -493,12 +493,12 @@ 反馈通知 隐藏嘟文的统计信息 限制时间线通知 - 一些可能影响您精神状态的信息将被隐藏,这些信息包括: -\n -\n - 收藏、转发、关注通知 -\n - 收藏、转发数 -\n - 账号的已关注数量、嘟文数量 -\n + 一些可能影响您精神状态的信息将被隐藏,这些信息包括: +\n +\n - 喜欢、转发、关注通知 +\n - 喜欢、转发数 +\n - 账号的已关注数量、嘟文数量 +\n \n 推送通知不会被影响,但可以在通知设置中手动禁用。 健康模式 永久 diff --git a/app/src/main/res/values-zh-rSG/strings.xml b/app/src/main/res/values-zh-rSG/strings.xml index 6e603e50f..4dfdaf49e 100644 --- a/app/src/main/res/values-zh-rSG/strings.xml +++ b/app/src/main/res/values-zh-rSG/strings.xml @@ -31,7 +31,7 @@ 已置顶 正在关注 关注者 - 收藏 + 喜欢 被隐藏的用户 被屏蔽的用户 关注请求 @@ -50,7 +50,7 @@ 还没有内容 还没有内容,向下拉动即可刷新 %s 转嘟了你的嘟文 - %s 收藏了你的嘟文 + %s 喜欢了你的嘟文 %s 关注了你 报告用户 @%s 的滥用行为 报告更多信息 @@ -58,8 +58,8 @@ 回复 转嘟 取消转嘟 - 收藏 - 取消收藏 + 喜欢 + 取消喜欢 更多 新嘟文 登录 Mastodon 帐号 @@ -81,7 +81,7 @@ 个人资料 设置 帐户设置 - 收藏 + 喜欢 被隐藏的用户 被屏蔽的用户 关注请求 @@ -112,7 +112,7 @@ 话题 打开转嘟用户主页 显示转嘟 - 显示收藏 + 显示喜欢 话题 提及 链接 @@ -168,7 +168,7 @@ 被提及 有新的关注者 嘟文被转嘟 - 嘟文被收藏 + 嘟文被喜欢 投票已结束 外观 应用主题 @@ -212,8 +212,8 @@ 当有用户关注我时 转嘟 当有用户转嘟了我的嘟文时 - 收藏 - 当有用户收藏了我的嘟文时 + 喜欢 + 当有用户喜欢了我的嘟文时 投票 当我参与的投票结束时 %s 提及了你 @@ -333,13 +333,13 @@ 取消置顶 置顶 - <b>%1$s</b> 次收藏 + %1$s 次喜欢 <b>%s</b> 次转嘟 转嘟 - 收藏 + 喜欢 %1$s %1$s 和 %2$s %1$s, %2$s 和 %3$d 等人 @@ -358,9 +358,7 @@ 被转嘟 - - 被收藏 - + 被喜欢 公开 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 6ccd6b8e2..1f9053c70 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -50,7 +50,7 @@ 沒有內容。 還沒有內容,向下拉動即可重新整理! %s 轉嘟了你的嘟文 - %s 收藏了你的嘟文 + %s 最愛了你的嘟文 %s 關注了你 檢舉使用者 @%s 的濫用行為 更多評論? @@ -58,8 +58,8 @@ 回覆 轉嘟 取消轉嘟 - 收藏 - 取消收藏 + 最愛 + 取消最愛 更多 撰寫嘟文 登入 Mastodon 帳號 @@ -81,7 +81,7 @@ 個人資料 設定 帳戶設定 - 我的收藏 + 我的最愛 被靜音的使用者 被封鎖的使用者 關注請求 @@ -171,7 +171,7 @@ 被提及 有新的關注者 嘟文被轉嘟 - 嘟文被加入收藏 + 嘟文被加入最愛 投票已結束 外觀 佈景主題 @@ -215,8 +215,8 @@ 當有使用者關注我時 轉嘟 當有使用者轉嘟了我的嘟文時 - 收藏 - 當有使用者把我的嘟文加入收藏時 + 最愛 + 當有使用者把我的嘟文加入最愛時 投票 當我參與的投票結束時 %s 提及了你 @@ -335,13 +335,13 @@ 取消置頂 置頂 - <b>%1$s</b> 次收藏 + %1$s 次最愛 <b>%s</b> 次轉嘟 轉嘟 - 收藏由 + 最愛由 %1$s %1$s 和 %2$s %1$s, %2$s 和 %3$d 等人 @@ -360,9 +360,7 @@ 被轉嘟 - - 被收藏 - + 被最愛 公開 @@ -444,8 +442,8 @@ 檢查通知設定 有些資訊可能會影響你的心理健康將會被隱藏。包括: \n -\n- 收藏/轉嘟/關注 通知 -\n- 收藏/轉嘟 數量 +\n- 最愛/轉嘟/關注 通知 +\n- 最愛/轉嘟 數量 \n- 關注/貼文 在個人頁面的狀態 \n \n推播通知不會受到影響,但你可以手動檢查你的通知設定。 From 6251aa0d1458ceb2a03786a5b5e41f7fc002dc99 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Thu, 2 Jun 2022 09:40:43 +0000 Subject: [PATCH 34/82] Translated using Weblate (Ukrainian) Currently translated at 100.0% (488 of 488 strings) Co-authored-by: Ihor Hordiichuk Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translation: Tusky/Tusky --- app/src/main/res/values-uk/strings.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 3f49aaed9..60af4f09e 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -557,4 +557,7 @@ Щоб використовувати push-сповіщення через UnifiedPush, Tusky потребує дозволу стежити за сповіщеннями на вашому сервері Mastodon. Це вимагає повторного входу, щоб змінити області OAuth, надані Tusky. Використання параметра повторного входу тут або в налаштуваннях облікового запису збереже всі ваші локальні чернетки та кеш. Ви повторно увійшли до свого поточного облікового запису, щоб надати дозвіл на стеження Tusky. Однак у вас все ще є інші облікові записи, які не мігрували таким чином. Перейдіть до них і повторно увійдіть до них по одному, щоб забезпечити підтримку UnifiedPush сповіщень. Приєднується %1$s + Редагувати зображення + 1+ + Неможливо редагувати вкладення. \ No newline at end of file From 45a77b8d0535003e3364e80af4db70c4b6661460 Mon Sep 17 00:00:00 2001 From: XoseM Date: Thu, 2 Jun 2022 09:40:44 +0000 Subject: [PATCH 35/82] Translated using Weblate (Galician) Currently translated at 100.0% (488 of 488 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 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 8b7f6687f..98f307f45 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -535,4 +535,7 @@ Detalles Non se puido cargar a páxina de inicio. Gardando borrador… + 1+ + Non se puido editar o anexo. + Editar imaxe \ No newline at end of file From dcc04fb640b5f250a08cc981c4ca9aad95595e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sveinn=20=C3=AD=20Felli?= Date: Thu, 2 Jun 2022 09:40:44 +0000 Subject: [PATCH 36/82] Translated using Weblate (Icelandic) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (488 of 488 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 | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 0177ee5c1..57ca37936 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -525,4 +525,17 @@ Tilkynningar um nýja notendur Breytingar á færslum Tilkynningar þegar færslum sem þú hefur átt við er breytt + Þú hefur skráð þig aftur inn í fyrirliggjandi aðganginn þinn til þess að veita heimild fyrir áskrift að ýti-tilkynningum í Tusky. Aftur á móti ertu með aðra aðganga sem ekki hafa verið yfirfærðir á þennan hátt. Skiptu yfir í þá og skráðu þig þar inn aftur til að virkja stuðning við tilkynningar í gegnum UnifiedPush. + Skráði sig %1$s + Skrá aftur inn alla aðganga til að virkja stuðning við ýti-tilkynningar. + Til þess að geta sent ýti-tilkynningar í gegnum UnifiedPush, þarf Tusky heimild til að gerast áskrifandi að tilkynningum á Mastodon-netþjóninum þínum. Þetta krefst þess að skráð sé inn aftur til að breyta vægi OAuth-heimilda sem Tusky er úthlutað. Notaðu endurinnskráninguna hérna eða í kjörstillingum aðgangsins þíns til að varðveita öll drögin þín og skyndiminni á tækinu. + Skrá inn + 1+ + Gat ekki lesið innskráningarsíðuna. + Ekki var hægt að breyta viðhenginu. + Breyta mynd + Vista drög… + Skráðu aftur inn fyrir ýti-tilkynningar + Hunsa + Nánar \ No newline at end of file From cbc5077b1f28f6c3a1c6f1e86caa9b70f0381c50 Mon Sep 17 00:00:00 2001 From: Karmanyaah Malhotra <32671690+karmanyaahm@users.noreply.github.com> Date: Thu, 9 Jun 2022 14:03:27 -0400 Subject: [PATCH 37/82] UnifiedPush: FEATURE_BYTES_MESSAGE distributors (#2581) Only use distributors which are compatible with FEATURE_BYTES_MESSAGE This is because Mastodon sends notifications that are arbritary bytes, not UTF-8. This causes issues in older versions of UnifiedPush distributors and providers that don't support FEATURE_BYTES_MESSAGE --- .../tusky/components/notifications/PushNotificationHelper.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt index a819b282c..f1b714301 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt @@ -89,7 +89,7 @@ private suspend fun enableUnifiedPushNotificationsForAccount(context: Context, a // Already registered, update the subscription to match notification settings updateUnifiedPushSubscription(context, api, accountManager, account) } else { - UnifiedPush.registerAppWithDialog(context, account.id.toString()) + UnifiedPush.registerAppWithDialog(context, account.id.toString(), features = arrayListOf(UnifiedPush.FEATURE_BYTES_MESSAGE)) } } From fd568aedba98093196639c1de0ecca4951d0e3ce Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 16 Jun 2022 18:50:01 +0200 Subject: [PATCH 38/82] fix refreshing notifications screen when there are no notifications (#2583) --- .../keylesspalace/tusky/fragment/NotificationsFragment.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 080ba4ca0..aed121a17 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -963,10 +963,10 @@ public class NotificationsFragment extends SFragment implements if (notifications.size() == 0 && adapter.getItemCount() == 0) { this.statusView.setVisibility(View.VISIBLE); this.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, null); - } else { - swipeRefreshLayout.setEnabled(true); } + updateFilterVisibility(); + swipeRefreshLayout.setEnabled(true); swipeRefreshLayout.setRefreshing(false); progressBar.setVisibility(View.GONE); } From 3417a7272aaa8f633575fac47d662a804f8f677a Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 16 Jun 2022 18:51:35 +0200 Subject: [PATCH 39/82] use moshis Rfc3339DateJsonAdapter for json date parsing (#2584) * Rename .java to .kt * use moshis Rfc3339DateJsonAdapter for json date parsing --- .../keylesspalace/tusky/di/NetworkModule.kt | 4 +- .../keylesspalace/tusky/json/Iso8601Utils.kt | 268 +++++++++++++++++ .../tusky/json/Rfc3339DateJsonAdapter.kt | 49 +++ .../tusky/json/UtcDateTypeAdapter.java | 284 ------------------ 4 files changed, 319 insertions(+), 286 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/json/Iso8601Utils.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/json/Rfc3339DateJsonAdapter.kt delete mode 100644 app/src/main/java/com/keylesspalace/tusky/json/UtcDateTypeAdapter.java 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 61c4ac88b..8250e61f7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -23,7 +23,7 @@ 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.UtcDateTypeAdapter +import com.keylesspalace.tusky.json.Rfc3339DateJsonAdapter import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.MediaUploadApi @@ -54,7 +54,7 @@ class NetworkModule { @Provides @Singleton fun providesGson(): Gson = GsonBuilder() - .registerTypeAdapter(Date::class.java, UtcDateTypeAdapter()) + .registerTypeAdapter(Date::class.java, Rfc3339DateJsonAdapter()) .create() @Provides diff --git a/app/src/main/java/com/keylesspalace/tusky/json/Iso8601Utils.kt b/app/src/main/java/com/keylesspalace/tusky/json/Iso8601Utils.kt new file mode 100644 index 000000000..bd8df6b5e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/json/Iso8601Utils.kt @@ -0,0 +1,268 @@ +package com.keylesspalace.tusky.json + +/* + * Copyright (C) 2011 FasterXML, LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.google.gson.JsonParseException +import java.util.Calendar +import java.util.Date +import java.util.GregorianCalendar +import java.util.Locale +import java.util.TimeZone +import kotlin.math.min +import kotlin.math.pow + +/* + * Jackson’s date formatter, pruned to Moshi's needs. Forked from this file: + * https://github.com/FasterXML/jackson-databind/blob/67ebf7305f492285a8f9f4de31545f5f16fc7c3a/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java + * + * Utilities methods for manipulating dates in iso8601 format. This is much much faster and GC + * friendly than using SimpleDateFormat so highly suitable if you (un)serialize lots of date + * objects. + * + * Supported parse format: + * `[yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh[:]mm]]` + * + * @see [this specification](http://www.w3.org/TR/NOTE-datetime) + */ + +/** ID to represent the 'GMT' string */ +private const val GMT_ID = "GMT" + +/** The GMT timezone, prefetched to avoid more lookups. */ +private val TIMEZONE_Z: TimeZone = TimeZone.getTimeZone(GMT_ID) + +/** Returns `date` formatted as yyyy-MM-ddThh:mm:ss.sssZ */ +internal fun Date.formatIsoDate(): String { + val calendar: Calendar = GregorianCalendar(TIMEZONE_Z, Locale.US) + calendar.time = this + + // estimate capacity of buffer as close as we can (yeah, that's pedantic ;) + val capacity = "yyyy-MM-ddThh:mm:ss.sssZ".length + val formatted = StringBuilder(capacity) + padInt(formatted, calendar[Calendar.YEAR], "yyyy".length) + formatted.append('-') + padInt(formatted, calendar[Calendar.MONTH] + 1, "MM".length) + formatted.append('-') + padInt(formatted, calendar[Calendar.DAY_OF_MONTH], "dd".length) + formatted.append('T') + padInt(formatted, calendar[Calendar.HOUR_OF_DAY], "hh".length) + formatted.append(':') + padInt(formatted, calendar[Calendar.MINUTE], "mm".length) + formatted.append(':') + padInt(formatted, calendar[Calendar.SECOND], "ss".length) + formatted.append('.') + padInt(formatted, calendar[Calendar.MILLISECOND], "sss".length) + formatted.append('Z') + return formatted.toString() +} + +/** + * Parse a date from ISO-8601 formatted string. It expects a format + * `[yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]` + * + * @receiver ISO string to parse in the appropriate format. + * @return the parsed date + */ +internal fun String.parseIsoDate(): Date { + return try { + var offset = 0 + + // extract year + val year = parseInt(this, offset, 4.let { offset += it; offset }) + if (checkOffset(this, offset, '-')) { + offset += 1 + } + + // extract month + val month = parseInt(this, offset, 2.let { offset += it; offset }) + if (checkOffset(this, offset, '-')) { + offset += 1 + } + + // extract day + val day = parseInt(this, offset, 2.let { offset += it; offset }) + // default time value + var hour = 0 + var minutes = 0 + var seconds = 0 + // always use 0 otherwise returned date will include millis of current time + var milliseconds = 0 + + // if the value has no time component (and no time zone), we are done + val hasT = checkOffset(this, offset, 'T') + if (!hasT && this.length <= offset) { + return GregorianCalendar(year, month - 1, day).time + } + if (hasT) { + + // extract hours, minutes, seconds and milliseconds + hour = parseInt(this, 1.let { offset += it; offset }, 2.let { offset += it; offset }) + if (checkOffset(this, offset, ':')) { + offset += 1 + } + minutes = parseInt(this, offset, 2.let { offset += it; offset }) + if (checkOffset(this, offset, ':')) { + offset += 1 + } + // second and milliseconds can be optional + if (this.length > offset) { + val c = this[offset] + if (c != 'Z' && c != '+' && c != '-') { + seconds = parseInt(this, offset, 2.let { offset += it; offset }) + if (seconds in 60..62) seconds = 59 // truncate up to 3 leap seconds + // milliseconds can be optional in the format + if (checkOffset(this, offset, '.')) { + offset += 1 + val endOffset = indexOfNonDigit(this, offset + 1) // assume at least one digit + val parseEndOffset = min(endOffset, offset + 3) // parse up to 3 digits + val fraction = parseInt(this, offset, parseEndOffset) + milliseconds = + (10.0.pow((3 - (parseEndOffset - offset)).toDouble()) * fraction).toInt() + offset = endOffset + } + } + } + } + + // extract timezone + require(this.length > offset) { "No time zone indicator" } + val timezone: TimeZone + val timezoneIndicator = this[offset] + if (timezoneIndicator == 'Z') { + timezone = TIMEZONE_Z + } else if (timezoneIndicator == '+' || timezoneIndicator == '-') { + val timezoneOffset = this.substring(offset) + // 18-Jun-2015, tatu: Minor simplification, skip offset of "+0000"/"+00:00" + if ("+0000" == timezoneOffset || "+00:00" == timezoneOffset) { + timezone = TIMEZONE_Z + } else { + // 18-Jun-2015, tatu: Looks like offsets only work from GMT, not UTC... + // not sure why, but it is what it is. + val timezoneId = GMT_ID + timezoneOffset + timezone = TimeZone.getTimeZone(timezoneId) + val act = timezone.id + if (act != timezoneId) { + /* + * 22-Jan-2015, tatu: Looks like canonical version has colons, but we may be given + * one without. If so, don't sweat. + * Yes, very inefficient. Hopefully not hit often. + * If it becomes a perf problem, add 'loose' comparison instead. + */ + val cleaned = act.replace(":", "") + if (cleaned != timezoneId) { + throw IndexOutOfBoundsException( + "Mismatching time zone indicator: $timezoneId given, resolves to ${timezone.id}" + ) + } + } + } + } else { + throw IndexOutOfBoundsException( + "Invalid time zone indicator '$timezoneIndicator'" + ) + } + val calendar: Calendar = GregorianCalendar(timezone) + calendar.isLenient = false + calendar[Calendar.YEAR] = year + calendar[Calendar.MONTH] = month - 1 + calendar[Calendar.DAY_OF_MONTH] = day + calendar[Calendar.HOUR_OF_DAY] = hour + calendar[Calendar.MINUTE] = minutes + calendar[Calendar.SECOND] = seconds + calendar[Calendar.MILLISECOND] = milliseconds + calendar.time + // If we get a ParseException it'll already have the right message/offset. + // Other exception types can convert here. + } catch (e: IndexOutOfBoundsException) { + throw JsonParseException("Not an RFC 3339 date: $this", e) + } catch (e: IllegalArgumentException) { + throw JsonParseException("Not an RFC 3339 date: $this", e) + } +} + +/** + * Check if the expected character exist at the given offset in the value. + * + * @param value the string to check at the specified offset + * @param offset the offset to look for the expected character + * @param expected the expected character + * @return true if the expected character exist at the given offset + */ +private fun checkOffset(value: String, offset: Int, expected: Char): Boolean { + return offset < value.length && value[offset] == expected +} + +/** + * Parse an integer located between 2 given offsets in a string + * + * @param value the string to parse + * @param beginIndex the start index for the integer in the string + * @param endIndex the end index for the integer in the string + * @return the int + * @throws NumberFormatException if the value is not a number + */ +private fun parseInt(value: String, beginIndex: Int, endIndex: Int): Int { + if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) { + throw NumberFormatException(value) + } + // use same logic as in Integer.parseInt() but less generic we're not supporting negative values + var i = beginIndex + var result = 0 + var digit: Int + if (i < endIndex) { + digit = Character.digit(value[i++], 10) + if (digit < 0) { + throw NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex)) + } + result = -digit + } + while (i < endIndex) { + digit = Character.digit(value[i++], 10) + if (digit < 0) { + throw NumberFormatException("Invalid number: " + value.substring(beginIndex, endIndex)) + } + result *= 10 + result -= digit + } + return -result +} + +/** + * Zero pad a number to a specified length + * + * @param buffer buffer to use for padding + * @param value the integer value to pad if necessary. + * @param length the length of the string we should zero pad + */ +private fun padInt(buffer: StringBuilder, value: Int, length: Int) { + val strValue = value.toString() + for (i in length - strValue.length downTo 1) { + buffer.append('0') + } + buffer.append(strValue) +} + +/** + * Returns the index of the first character in the string that is not a digit, starting at offset. + */ +private fun indexOfNonDigit(string: String, offset: Int): Int { + for (i in offset until string.length) { + val c = string[i] + if (c < '0' || c > '9') return i + } + return string.length +} diff --git a/app/src/main/java/com/keylesspalace/tusky/json/Rfc3339DateJsonAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/json/Rfc3339DateJsonAdapter.kt new file mode 100644 index 000000000..090fe5e37 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/json/Rfc3339DateJsonAdapter.kt @@ -0,0 +1,49 @@ +// https://github.com/google/gson/blob/master/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java +/* + * Copyright (C) 2011 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.keylesspalace.tusky.json + +import com.google.gson.TypeAdapter +import com.google.gson.stream.JsonReader +import com.google.gson.stream.JsonToken +import com.google.gson.stream.JsonWriter +import java.io.IOException +import java.util.Date + +class Rfc3339DateJsonAdapter : TypeAdapter() { + + @Throws(IOException::class) + override fun write(writer: JsonWriter, date: Date?) { + if (date == null) { + writer.nullValue() + } else { + writer.value(date.formatIsoDate()) + } + } + + @Throws(IOException::class) + override fun read(reader: JsonReader): Date? { + return when (reader.peek()) { + JsonToken.NULL -> { + reader.nextNull() + null + } + else -> { + reader.nextString().parseIsoDate() + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/json/UtcDateTypeAdapter.java b/app/src/main/java/com/keylesspalace/tusky/json/UtcDateTypeAdapter.java deleted file mode 100644 index d178b3fc9..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/json/UtcDateTypeAdapter.java +++ /dev/null @@ -1,284 +0,0 @@ -// https://github.com/google/gson/blob/master/extras/src/main/java/com/google/gson/typeadapters/UtcDateTypeAdapter.java - -/* - * Copyright (C) 2011 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.keylesspalace.tusky.json; - -import java.io.IOException; -import java.text.ParseException; -import java.text.ParsePosition; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.Locale; -import java.util.TimeZone; - -import com.google.gson.JsonParseException; -import com.google.gson.TypeAdapter; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -public final class UtcDateTypeAdapter extends TypeAdapter { - private final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC"); - - @Override - public void write(JsonWriter out, Date date) throws IOException { - if (date == null) { - out.nullValue(); - } else { - String value = format(date, true, UTC_TIME_ZONE); - out.value(value); - } - } - - @Override - public Date read(JsonReader in) throws IOException { - try { - switch (in.peek()) { - case NULL: - in.nextNull(); - return null; - default: - String date = in.nextString(); - // Instead of using iso8601Format.parse(value), we use Jackson's date parsing - // This is because Android doesn't support XXX because it is JDK 1.6 - return parse(date, new ParsePosition(0)); - } - } catch (ParseException e) { - throw new JsonParseException(e); - } - } - - // Date parsing code from Jackson databind ISO8601Utils.java - // https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java - private static final String GMT_ID = "GMT"; - - /** - * Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm] - * - * @param date the date to format - * @param millis true to include millis precision otherwise false - * @param tz timezone to use for the formatting (GMT will produce 'Z') - * @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm] - */ - private static String format(Date date, boolean millis, TimeZone tz) { - Calendar calendar = new GregorianCalendar(tz, Locale.US); - calendar.setTime(date); - - // estimate capacity of buffer as close as we can (yeah, that's pedantic ;) - int capacity = "yyyy-MM-ddThh:mm:ss".length(); - capacity += millis ? ".sss".length() : 0; - capacity += tz.getRawOffset() == 0 ? "Z".length() : "+hh:mm".length(); - StringBuilder formatted = new StringBuilder(capacity); - - padInt(formatted, calendar.get(Calendar.YEAR), "yyyy".length()); - formatted.append('-'); - padInt(formatted, calendar.get(Calendar.MONTH) + 1, "MM".length()); - formatted.append('-'); - padInt(formatted, calendar.get(Calendar.DAY_OF_MONTH), "dd".length()); - formatted.append('T'); - padInt(formatted, calendar.get(Calendar.HOUR_OF_DAY), "hh".length()); - formatted.append(':'); - padInt(formatted, calendar.get(Calendar.MINUTE), "mm".length()); - formatted.append(':'); - padInt(formatted, calendar.get(Calendar.SECOND), "ss".length()); - if (millis) { - formatted.append('.'); - padInt(formatted, calendar.get(Calendar.MILLISECOND), "sss".length()); - } - - int offset = tz.getOffset(calendar.getTimeInMillis()); - if (offset != 0) { - int hours = Math.abs((offset / (60 * 1000)) / 60); - int minutes = Math.abs((offset / (60 * 1000)) % 60); - formatted.append(offset < 0 ? '-' : '+'); - padInt(formatted, hours, "hh".length()); - formatted.append(':'); - padInt(formatted, minutes, "mm".length()); - } else { - formatted.append('Z'); - } - - return formatted.toString(); - } - /** - * Zero pad a number to a specified length - * - * @param buffer buffer to use for padding - * @param value the integer value to pad if necessary. - * @param length the length of the string we should zero pad - */ - private static void padInt(StringBuilder buffer, int value, int length) { - String strValue = Integer.toString(value); - for (int i = length - strValue.length(); i > 0; i--) { - buffer.append('0'); - } - buffer.append(strValue); - } - - /** - * Parse a date from ISO-8601 formatted string. It expects a format - * [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]] - * - * @param date ISO string to parse in the appropriate format. - * @param pos The position to start parsing from, updated to where parsing stopped. - * @return the parsed date - * @throws ParseException if the date is not in the appropriate format - */ - private static Date parse(String date, ParsePosition pos) throws ParseException { - Exception fail = null; - try { - int offset = pos.getIndex(); - - // extract year - int year = parseInt(date, offset, offset += 4); - if (checkOffset(date, offset, '-')) { - offset += 1; - } - - // extract month - int month = parseInt(date, offset, offset += 2); - if (checkOffset(date, offset, '-')) { - offset += 1; - } - - // extract day - int day = parseInt(date, offset, offset += 2); - // default time value - int hour = 0; - int minutes = 0; - int seconds = 0; - int milliseconds = 0; // always use 0 otherwise returned date will include millis of current time - if (checkOffset(date, offset, 'T')) { - - // extract hours, minutes, seconds and milliseconds - hour = parseInt(date, offset += 1, offset += 2); - if (checkOffset(date, offset, ':')) { - offset += 1; - } - - minutes = parseInt(date, offset, offset += 2); - if (checkOffset(date, offset, ':')) { - offset += 1; - } - // second and milliseconds can be optional - if (date.length() > offset) { - char c = date.charAt(offset); - if (c != 'Z' && c != '+' && c != '-') { - seconds = parseInt(date, offset, offset += 2); - // milliseconds can be optional in the format - if (checkOffset(date, offset, '.')) { - milliseconds = parseInt(date, offset += 1, offset += 3); - } - } - } - } - - // extract timezone - String timezoneId; - if (date.length() <= offset) { - throw new IllegalArgumentException("No time zone indicator"); - } - char timezoneIndicator = date.charAt(offset); - if (timezoneIndicator == '+' || timezoneIndicator == '-') { - String timezoneOffset = date.substring(offset); - timezoneId = GMT_ID + timezoneOffset; - offset += timezoneOffset.length(); - } else if (timezoneIndicator == 'Z') { - timezoneId = GMT_ID; - offset += 1; - } else { - throw new IndexOutOfBoundsException("Invalid time zone indicator " + timezoneIndicator); - } - - TimeZone timezone = TimeZone.getTimeZone(timezoneId); - if (!timezone.getID().equals(timezoneId)) { - throw new IndexOutOfBoundsException(); - } - - Calendar calendar = new GregorianCalendar(timezone); - calendar.setLenient(false); - calendar.set(Calendar.YEAR, year); - calendar.set(Calendar.MONTH, month - 1); - calendar.set(Calendar.DAY_OF_MONTH, day); - calendar.set(Calendar.HOUR_OF_DAY, hour); - calendar.set(Calendar.MINUTE, minutes); - calendar.set(Calendar.SECOND, seconds); - calendar.set(Calendar.MILLISECOND, milliseconds); - - pos.setIndex(offset); - return calendar.getTime(); - // If we get a ParseException it'll already have the right message/offset. - // Other exception types can convert here. - } catch (IndexOutOfBoundsException e) { - fail = e; - } catch (NumberFormatException e) { - fail = e; - } catch (IllegalArgumentException e) { - fail = e; - } - String input = (date == null) ? null : ("'" + date + "'"); - throw new ParseException("Failed to parse date [" + input + "]: " + fail.getMessage(), pos.getIndex()); - } - - /** - * Check if the expected character exist at the given offset in the value. - * - * @param value the string to check at the specified offset - * @param offset the offset to look for the expected character - * @param expected the expected character - * @return true if the expected character exist at the given offset - */ - private static boolean checkOffset(String value, int offset, char expected) { - return (offset < value.length()) && (value.charAt(offset) == expected); - } - - /** - * Parse an integer located between 2 given offsets in a string - * - * @param value the string to parse - * @param beginIndex the start index for the integer in the string - * @param endIndex the end index for the integer in the string - * @return the int - * @throws NumberFormatException if the value is not a number - */ - private static int parseInt(String value, int beginIndex, int endIndex) throws NumberFormatException { - if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) { - throw new NumberFormatException(value); - } - // use same logic as in Integer.parseInt() but less generic we're not supporting negative values - int i = beginIndex; - int result = 0; - int digit; - if (i < endIndex) { - digit = Character.digit(value.charAt(i++), 10); - if (digit < 0) { - throw new NumberFormatException("Invalid number: " + value); - } - result = -digit; - } - while (i < endIndex) { - digit = Character.digit(value.charAt(i++), 10); - if (digit < 0) { - throw new NumberFormatException("Invalid number: " + value); - } - result *= 10; - result -= digit; - } - return -result; - } -} From eeab688d9d8136ed85f3cd14fecb2c58b5cbdeaa Mon Sep 17 00:00:00 2001 From: hebbeff Date: Tue, 7 Jun 2022 17:40:45 +0000 Subject: [PATCH 40/82] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (488 of 488 strings) Co-authored-by: hebbeff Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/ Translation: Tusky/Tusky --- app/src/main/res/values-zh-rCN/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 74054078e..6a8223629 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -337,7 +337,7 @@ 取消置顶 置顶 - %1$s 次喜欢 + <b>%1$s</b> 次喜欢 <b>%s</b> 次转嘟 From 1a29a589e1256b4d55f7a97d4f2f83704772c220 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 20 Jun 2022 16:08:19 +0200 Subject: [PATCH 41/82] rewrite InstanceDao queries to drop unused columns (#2585) --- app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt | 3 +++ 1 file changed, 3 insertions(+) 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 9b190bc7f..3687da09e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceDao.kt @@ -19,6 +19,7 @@ import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query +import androidx.room.RewriteQueriesToDropUnusedColumns @Dao interface InstanceDao { @@ -29,9 +30,11 @@ interface InstanceDao { @Insert(onConflict = OnConflictStrategy.REPLACE, entity = InstanceEntity::class) suspend fun insertOrReplace(emojis: EmojisEntity) + @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1") suspend fun getInstanceInfo(instance: String): InstanceInfoEntity? + @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1") suspend fun getEmojiInfo(instance: String): EmojisEntity? } From 2e33233309d6ea0b1fd3716df01dd17bc80573c6 Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Mon, 20 Jun 2022 16:11:07 +0200 Subject: [PATCH 42/82] Don't display bot badge in account selection dialog (#2589) --- .../com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt index 4f58b1ffd..0b0115c2a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt @@ -44,6 +44,7 @@ class AccountSelectionAdapter(context: Context) : ArrayAdapter(co binding.username.text = account.fullName binding.displayName.text = account.displayName.emojify(account.emojis, binding.displayName, animateEmojis) + binding.avatarBadge.visibility = View.GONE // We never want to display the bot badge here val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp) val animateAvatar = pm.getBoolean("animateGifAvatars", false) From dba2fbc5c177029ef5accf4188f13b83c02e3cba Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 20 Jun 2022 16:11:30 +0200 Subject: [PATCH 43/82] fix empty timeline on initial load (#2586) --- .../timeline/viewmodel/CachedTimelineViewModel.kt | 7 +++++-- .../timeline/viewmodel/NetworkTimelineViewModel.kt | 2 +- .../components/timeline/viewmodel/TimelineViewModel.kt | 2 +- .../main/java/com/keylesspalace/tusky/db/TimelineDao.kt | 3 +++ 4 files changed, 10 insertions(+), 4 deletions(-) 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 a3fd4ec08..bec96234a 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 @@ -267,8 +267,11 @@ class CachedTimelineViewModel @Inject constructor( } } - override fun invalidate() { - currentPagingSource?.invalidate() + override suspend fun invalidate() { + // invalidating when we don't have statuses yet can cause empty timelines because it cancels the network load + if (db.timelineDao().getStatusCount(accountManager.activeAccount!!.id) > 0) { + currentPagingSource?.invalidate() + } } companion object { 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 5c2b4acda..9b9ee5b9f 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 @@ -249,7 +249,7 @@ class NetworkTimelineViewModel @Inject constructor( currentSource?.invalidate() } - override fun invalidate() { + override suspend fun invalidate() { currentSource?.invalidate() } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt index 79fe885ac..32c430f6e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt @@ -174,7 +174,7 @@ abstract class TimelineViewModel( abstract fun fullReload() /** Triggered when currently displayed data must be reloaded. */ - protected abstract fun invalidate() + protected abstract suspend fun invalidate() protected fun shouldFilterStatus(statusViewData: StatusViewData): Boolean { val status = statusViewData.asStatusOrNull()?.status ?: return false 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 123ebe211..210bfca3f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineDao.kt @@ -197,4 +197,7 @@ AND timelineUserId = :accountId */ @Query("SELECT serverId FROM TimelineStatusEntity WHERE timelineUserId = :accountId AND authorServerId IS NULL AND (LENGTH(:serverId) > LENGTH(serverId) OR (LENGTH(:serverId) = LENGTH(serverId) AND :serverId > serverId)) ORDER BY LENGTH(serverId) DESC, serverId DESC LIMIT 1") abstract suspend fun getNextPlaceholderIdAfter(accountId: Long, serverId: String): String? + + @Query("SELECT COUNT(*) FROM TimelineStatusEntity WHERE timelineUserId = :accountId") + abstract suspend fun getStatusCount(accountId: Long): Int } From e0f5c35eff563639270e4cdb1b1b901608fabf7d Mon Sep 17 00:00:00 2001 From: XoseM Date: Sun, 19 Jun 2022 03:59:46 +0000 Subject: [PATCH 44/82] Translated using Weblate (Galician) Currently translated at 100.0% (488 of 488 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 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 98f307f45..1d2c75f05 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -52,8 +52,8 @@ Bloquear Deixar de seguir Seguir - Tes a certeza de que queres desconectar a conta %1$s\? - Desconectar + Tes a certeza de que queres pechar sesión da conta %1$s\? + Pechar sesión Accede con Mastodon Redactar Máis @@ -524,7 +524,7 @@ Foi editada unha publicación coa que interactuei Edicións da publicación Creada %1$s - Volver a conectar tódalas contas para activar as notificacións push. + Volve a acceder con tódalas contas para activar as notificacións push. Acceder Notificacións cando son editadas publicacións coas que interactuaches Para poder usar as notificacións push vía UnifiedPush, Tusky require o permiso para subscribirse ás notificacións do teu servidor Mastodon. É necesario volver a acceder para cambiar os ámbitos OAuth concedidos a Tusky. Usando aquí ou nas Preferencias da Conta a opción de volver a acceder conservarás os borradores locais e caché. From f419e83c16438456559163463785ec68ef98fdd3 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 20 Jun 2022 16:45:54 +0200 Subject: [PATCH 45/82] improve logout (#2579) * improve logout * fix tests * add db migration * delete wrongly committed file again * improve LogoutUsecase --- .../com/keylesspalace/tusky/MainActivity.kt | 41 ++++-------- .../tusky/appstore/CacheUpdater.kt | 6 +- .../conversation/ConversationsRepository.kt | 30 --------- .../conversation/ConversationsViewModel.kt | 2 +- .../tusky/components/login/LoginActivity.kt | 8 ++- .../components/search/SearchViewModel.kt | 2 +- .../viewmodel/CachedTimelineRemoteMediator.kt | 4 ++ .../viewmodel/CachedTimelineViewModel.kt | 2 +- .../viewmodel/NetworkTimelineViewModel.kt | 2 +- .../timeline/viewmodel/TimelineViewModel.kt | 2 +- .../keylesspalace/tusky/db/AccountEntity.kt | 11 ++++ .../keylesspalace/tusky/db/AccountManager.kt | 30 ++++++--- .../keylesspalace/tusky/db/AppDatabase.java | 10 ++- .../com/keylesspalace/tusky/di/AppModule.kt | 3 +- .../tusky/fragment/SFragment.java | 2 +- .../InstanceSwitchAuthInterceptor.java | 30 ++++++--- .../tusky/network/MastodonApi.kt | 8 +++ .../tusky/usecase/LogoutUsecase.kt | 66 +++++++++++++++++++ .../{network => usecase}/TimelineCases.kt | 3 +- app/src/main/res/layout/activity_main.xml | 8 +++ .../tusky/ComposeActivityTest.kt | 2 + .../CachedTimelineRemoteMediatorTest.kt | 2 + .../NetworkTimelineRemoteMediatorTest.kt | 2 + 23 files changed, 185 insertions(+), 91 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt create mode 100644 app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt rename app/src/main/java/com/keylesspalace/tusky/{network => usecase}/TimelineCases.kt (98%) diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index 4fe8df6da..fd2e15b73 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -61,13 +61,10 @@ import com.keylesspalace.tusky.components.account.AccountActivity import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType -import com.keylesspalace.tusky.components.conversation.ConversationsRepository -import com.keylesspalace.tusky.components.drafts.DraftHelper import com.keylesspalace.tusky.components.drafts.DraftsActivity import com.keylesspalace.tusky.components.login.LoginActivity import com.keylesspalace.tusky.components.notifications.NotificationHelper import com.keylesspalace.tusky.components.notifications.disableAllNotifications -import com.keylesspalace.tusky.components.notifications.disableUnifiedPushNotificationsForAccount import com.keylesspalace.tusky.components.notifications.enablePushNotificationsWithFallback import com.keylesspalace.tusky.components.notifications.showMigrationNoticeIfNecessary import com.keylesspalace.tusky.components.preference.PreferencesActivity @@ -81,11 +78,12 @@ import com.keylesspalace.tusky.interfaces.ActionButtonActivity import com.keylesspalace.tusky.interfaces.ReselectableFragment import com.keylesspalace.tusky.pager.MainPagerAdapter import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.usecase.LogoutUsecase import com.keylesspalace.tusky.util.ThemeUtils import com.keylesspalace.tusky.util.deleteStaleCachedMedia import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.hide -import com.keylesspalace.tusky.util.removeShortcut +import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.updateShortcut import com.keylesspalace.tusky.util.viewBinding import com.keylesspalace.tusky.util.visible @@ -135,10 +133,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje lateinit var cacheUpdater: CacheUpdater @Inject - lateinit var conversationRepository: ConversationsRepository - - @Inject - lateinit var draftHelper: DraftHelper + lateinit var logoutUsecase: LogoutUsecase private val binding by viewBinding(ActivityMainBinding::inflate) @@ -664,28 +659,18 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje .setTitle(R.string.action_logout) .setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName)) .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> + binding.appBar.hide() + binding.viewPager.hide() + binding.progressBar.show() + binding.bottomNav.hide() + binding.composeButton.hide() + lifecycleScope.launch { - // Only disable UnifiedPush for this account -- do not call disableNotifications(), - // which unnecessarily disables it for all accounts and then re-enables it again at - // the next launch - disableUnifiedPushNotificationsForAccount(this@MainActivity, activeAccount) - NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, this@MainActivity) - cacheUpdater.clearForUser(activeAccount.id) - conversationRepository.deleteCacheForAccount(activeAccount.id) - draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id) - removeShortcut(this@MainActivity, activeAccount) - val newAccount = accountManager.logActiveAccountOut() - if (!NotificationHelper.areNotificationsEnabled( - this@MainActivity, - accountManager - ) - ) { - NotificationHelper.disablePullNotifications(this@MainActivity) - } - val intent = if (newAccount == null) { - LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT) - } else { + val otherAccountAvailable = logoutUsecase.logout() + val intent = if (otherAccountAvailable) { Intent(this@MainActivity, MainActivity::class.java) + } else { + LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT) } startActivity(intent) finishWithoutSlideOutAnimation() diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt index 12cb4a69e..66ae898b6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt @@ -9,7 +9,7 @@ import javax.inject.Inject class CacheUpdater @Inject constructor( eventHub: EventHub, private val accountManager: AccountManager, - private val appDatabase: AppDatabase, + appDatabase: AppDatabase, gson: Gson ) { @@ -44,8 +44,4 @@ class CacheUpdater @Inject constructor( fun stop() { this.disposable.dispose() } - - suspend fun clearForUser(accountId: Long) { - appDatabase.timelineDao().removeAll(accountId) - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt deleted file mode 100644 index 3f074be5b..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRepository.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* Copyright 2021 Tusky Contributors - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.components.conversation - -import com.keylesspalace.tusky.db.AppDatabase -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ConversationsRepository @Inject constructor( - val db: AppDatabase -) { - - suspend fun deleteCacheForAccount(accountId: Long) { - db.conversationDao().deleteForAccount(accountId) - } -} 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 684c6f011..735aa26c1 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 @@ -26,7 +26,7 @@ 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.usecase.TimelineCases import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await 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 4b82340b2..e55bbd71f 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 @@ -236,7 +236,13 @@ class LoginActivity : BaseActivity(), Injectable { domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code" ).fold( { accessToken -> - accountManager.addAccount(accessToken.accessToken, domain, OAUTH_SCOPES) + accountManager.addAccount( + accessToken = accessToken.accessToken, + domain = domain, + clientId = clientId, + clientSecret = clientSecret, + oauthScopes = OAUTH_SCOPES + ) val intent = Intent(this, MainActivity::class.java) intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index 065aa0407..af886cdd1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -27,7 +27,7 @@ import com.keylesspalace.tusky.entity.DeletedStatus import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.network.TimelineCases +import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.util.RxAwareViewModel import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.viewdata.StatusViewData diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt index c4aa2c72d..ebab4440a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt @@ -51,6 +51,10 @@ class CachedTimelineRemoteMediator( state: PagingState ): MediatorResult { + if (!activeAccount.isLoggedIn()) { + return MediatorResult.Success(endOfPaginationReached = true) + } + try { var dbEmpty = false 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 bec96234a..97bc625b4 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,7 @@ import com.keylesspalace.tusky.db.TimelineStatusWithAccount import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.network.TimelineCases +import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.viewdata.StatusViewData import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor 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 9b9ee5b9f..8c81df1db 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 @@ -34,7 +34,7 @@ import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.network.TimelineCases +import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.util.getDomain import com.keylesspalace.tusky.util.isLessThan import com.keylesspalace.tusky.util.isLessThanOrEqual diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt index 32c430f6e..d640f64f8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt @@ -39,8 +39,8 @@ import com.keylesspalace.tusky.entity.Filter import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.MastodonApi -import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.usecase.TimelineCases import com.keylesspalace.tusky.viewdata.StatusViewData import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow 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 0b717120c..5ffc9021d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt @@ -37,6 +37,8 @@ data class AccountEntity( @field:PrimaryKey(autoGenerate = true) var id: Long, val domain: String, var accessToken: String, + var clientId: String?, // nullable for backward compatibility + var clientSecret: String?, // nullable for backward compatibility var isActive: Boolean, var accountId: String = "", var username: String = "", @@ -81,6 +83,15 @@ data class AccountEntity( val fullName: String get() = "@$username@$domain" + fun logout() { + // deleting credentials so they cannot be used again + accessToken = "" + clientId = null + clientSecret = null + } + + fun isLoggedIn() = accessToken.isNotEmpty() + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt index 2ddbe5223..6048c855e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt @@ -54,7 +54,13 @@ class AccountManager @Inject constructor(db: AppDatabase) { * @param accessToken the access token for the new account * @param domain the domain of the accounts Mastodon instance */ - fun addAccount(accessToken: String, domain: String, oauthScopes: String) { + fun addAccount( + accessToken: String, + domain: String, + clientId: String, + clientSecret: String, + oauthScopes: String + ) { activeAccount?.let { it.isActive = false @@ -66,8 +72,13 @@ class AccountManager @Inject constructor(db: AppDatabase) { val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0 val newAccountId = maxAccountId + 1 activeAccount = AccountEntity( - id = newAccountId, domain = domain.lowercase(Locale.ROOT), - accessToken = accessToken, oauthScopes = oauthScopes, isActive = true + id = newAccountId, + domain = domain.lowercase(Locale.ROOT), + accessToken = accessToken, + clientId = clientId, + clientSecret = clientSecret, + oauthScopes = oauthScopes, + isActive = true ) } @@ -89,11 +100,12 @@ class AccountManager @Inject constructor(db: AppDatabase) { */ fun logActiveAccountOut(): AccountEntity? { - if (activeAccount == null) { - return null - } else { - accounts.remove(activeAccount!!) - accountDao.delete(activeAccount!!) + return activeAccount?.let { account -> + + account.logout() + + accounts.remove(account) + accountDao.delete(account) if (accounts.size > 0) { accounts[0].isActive = true @@ -103,7 +115,7 @@ class AccountManager @Inject constructor(db: AppDatabase) { } else { activeAccount = null } - return activeAccount + activeAccount } } 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 5bce53340..c43b36524 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 = 38) + }, version = 39) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -573,4 +573,12 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("DELETE FROM `TimelineStatusEntity`"); } }; + + public static final Migration MIGRATION_38_39 = new Migration(38, 39) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `clientId` TEXT"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `clientSecret` TEXT"); + } + }; } 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 b14c53922..e17cb3cf6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -64,7 +64,8 @@ class AppModule { 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_34_35, - AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38 + AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38, + AppDatabase.MIGRATION_38_39 ) .build() } 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 ad81abe33..01a08c203 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -56,7 +56,7 @@ import com.keylesspalace.tusky.di.Injectable; import com.keylesspalace.tusky.entity.Attachment; import com.keylesspalace.tusky.entity.Status; import com.keylesspalace.tusky.network.MastodonApi; -import com.keylesspalace.tusky.network.TimelineCases; +import com.keylesspalace.tusky.usecase.TimelineCases; import com.keylesspalace.tusky.util.LinkHelper; import com.keylesspalace.tusky.util.StatusParsingHelper; import com.keylesspalace.tusky.view.MuteAccountDialog; diff --git a/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java b/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java index 2dcedd873..a3e1a815f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java +++ b/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java @@ -15,6 +15,7 @@ package com.keylesspalace.tusky.network; +import android.util.Log; import androidx.annotation.NonNull; import com.keylesspalace.tusky.db.AccountEntity; @@ -22,22 +23,20 @@ import com.keylesspalace.tusky.db.AccountManager; import java.io.IOException; -import okhttp3.HttpUrl; -import okhttp3.Interceptor; -import okhttp3.Request; -import okhttp3.Response; +import okhttp3.*; /** * Created by charlag on 31/10/17. */ public final class InstanceSwitchAuthInterceptor implements Interceptor { - private AccountManager accountManager; + private final AccountManager accountManager; public InstanceSwitchAuthInterceptor(AccountManager accountManager) { this.accountManager = accountManager; } + @NonNull @Override public Response intercept(@NonNull Chain chain) throws IOException { @@ -55,13 +54,26 @@ public final class InstanceSwitchAuthInterceptor implements Interceptor { builder.url(swapHost(originalRequest.url(), instanceHeader)); builder.removeHeader(MastodonApi.DOMAIN_HEADER); } else if (currentAccount != null) { - //use domain of current account - builder.url(swapHost(originalRequest.url(), currentAccount.getDomain())) - .header("Authorization", - String.format("Bearer %s", currentAccount.getAccessToken())); + String accessToken = currentAccount.getAccessToken(); + if (!accessToken.isEmpty()) { + //use domain of current account + builder.url(swapHost(originalRequest.url(), currentAccount.getDomain())) + .header("Authorization", + String.format("Bearer %s", currentAccount.getAccessToken())); + } } Request newRequest = builder.build(); + if (MastodonApi.PLACEHOLDER_DOMAIN.equals(newRequest.url().host())) { + Log.w("ISAInterceptor", "no user logged in or no domain header specified - can't make request to " + newRequest.url()); + return new Response.Builder() + .code(400) + .message("Bad Request") + .protocol(Protocol.HTTP_2) + .body(ResponseBody.create("", MediaType.parse("text/plain"))) + .request(chain.request()) + .build(); + } return chain.proceed(newRequest); } else { 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 12d7cdcb4..b8834a54b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -459,6 +459,14 @@ interface MastodonApi { @Field("grant_type") grantType: String ): NetworkResult + @FormUrlEncoded + @POST("oauth/revoke") + suspend fun revokeOAuthToken( + @Field("client_id") clientId: String, + @Field("client_secret") clientSecret: String, + @Field("token") token: String + ): NetworkResult + @GET("/api/v1/lists") suspend fun getLists(): NetworkResult> diff --git a/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt b/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt new file mode 100644 index 000000000..f8d3b11ca --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt @@ -0,0 +1,66 @@ +package com.keylesspalace.tusky.usecase + +import android.content.Context +import com.keylesspalace.tusky.components.drafts.DraftHelper +import com.keylesspalace.tusky.components.notifications.NotificationHelper +import com.keylesspalace.tusky.components.notifications.disableUnifiedPushNotificationsForAccount +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.removeShortcut +import javax.inject.Inject + +class LogoutUsecase @Inject constructor( + private val context: Context, + private val api: MastodonApi, + private val db: AppDatabase, + private val accountManager: AccountManager, + private val draftHelper: DraftHelper +) { + + /** + * Logs the current account out and clears all caches associated with it + * @return true if the user is logged in with other accounts, false if it was the only one + */ + suspend fun logout(): Boolean { + accountManager.activeAccount?.let { activeAccount -> + + // invalidate the oauth token, if we have the client id & secret + // (could be missing if user logged in with a previous version of Tusky) + val clientId = activeAccount.clientId + val clientSecret = activeAccount.clientSecret + if (clientId != null && clientSecret != null) { + api.revokeOAuthToken( + clientId = clientId, + clientSecret = clientSecret, + token = activeAccount.accessToken + ) + } + + // disable push notifications + disableUnifiedPushNotificationsForAccount(context, activeAccount) + + // disable pull notifications + if (!NotificationHelper.areNotificationsEnabled(context, accountManager)) { + NotificationHelper.disablePullNotifications(context) + } + + // clear notification channels + NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, context) + + // remove account from local AccountManager + val otherAccountAvailable = accountManager.logActiveAccountOut() != null + + // clear the database - this could trigger network calls so do it last when all tokens are gone + db.timelineDao().removeAll(activeAccount.id) + db.conversationDao().deleteForAccount(activeAccount.id) + draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id) + + // remove shortcut associated with the account + removeShortcut(context, activeAccount) + + return otherAccountAvailable + } + return false + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt similarity index 98% rename from app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt rename to app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt index 86148e51a..8f1144340 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt @@ -13,7 +13,7 @@ * You should have received a copy of the GNU General Public License along with Tusky; if not, * see . */ -package com.keylesspalace.tusky.network +package com.keylesspalace.tusky.usecase import android.util.Log import com.keylesspalace.tusky.appstore.BlockEvent @@ -29,6 +29,7 @@ import com.keylesspalace.tusky.appstore.StatusDeletedEvent import com.keylesspalace.tusky.entity.DeletedStatus import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.disposables.CompositeDisposable import io.reactivex.rxjava3.kotlin.addTo diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 39fc717f9..2f8777092 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -13,6 +13,7 @@ tools:context="com.keylesspalace.tusky.MainActivity"> + + Date: Mon, 20 Jun 2022 16:52:01 +0200 Subject: [PATCH 46/82] add 39.json --- .../39.json | 887 ++++++++++++++++++ 1 file changed, 887 insertions(+) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/39.json diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/39.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/39.json new file mode 100644 index 000000000..be96b28a9 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/39.json @@ -0,0 +1,887 @@ +{ + "formatVersion": 1, + "database": { + "version": 39, + "identityHash": "ed3b752a3faec9d092d5ac0a2823d5d5", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, 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, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ed3b752a3faec9d092d5ac0a2823d5d5')" + ] + } +} \ No newline at end of file From 0375aa1d1886257360bf9be52935b3f54b1a847a Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Mon, 20 Jun 2022 17:05:22 +0200 Subject: [PATCH 47/82] release 92 --- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/93.txt | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/93.txt diff --git a/app/build.gradle b/app/build.gradle index a58748a5a..b1f08360e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,8 +24,8 @@ android { applicationId APP_ID minSdkVersion 21 targetSdkVersion 31 - versionCode 91 - versionName "18.0" + versionCode 92 + versionName "19.0 beta 1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/fastlane/metadata/android/en-US/changelogs/93.txt b/fastlane/metadata/android/en-US/changelogs/93.txt new file mode 100644 index 000000000..8dc15d853 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/93.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Support for Unified Push. To activate the support you will have to relogin into your accounts. +- The number of responses to a post is now indicated in timelines. +- Images can now by cropped while composing a post. +- Profiles now show the date when they were created. +- When viewing a list the title is now displayed in the toolbar. +- A lot of bugfixes +- Translation improvements \ No newline at end of file From 3ca8a0b549e6c74c8814ddc3e709b8667a5077f4 Mon Sep 17 00:00:00 2001 From: mcclure Date: Tue, 21 Jun 2022 16:14:11 -0400 Subject: [PATCH 48/82] Use specific term "image" in error UI, not "attachment" (#2592) The new message for the crop feature, "The attachment could not be edited.", turned out to be awkward in some languages (French) where according to the translator it would be better to more specifically say "The image could not be edited." (as currently we can only edit images). Patch replaces error_media_edit_failed with a error_image_edit_failed and deletes the existing error_media_edit_failed-s. --- .../keylesspalace/tusky/components/compose/ComposeActivity.kt | 2 +- app/src/main/res/values-fa/strings.xml | 1 - app/src/main/res/values-gd/strings.xml | 1 - app/src/main/res/values-gl/strings.xml | 1 - app/src/main/res/values-hu/strings.xml | 1 - app/src/main/res/values-is/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-no-rNB/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/strings.xml | 2 +- 12 files changed, 2 insertions(+), 12 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 c5bdf4654..1d4703406 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 @@ -177,7 +177,7 @@ class ComposeActivity : Log.w("ComposeActivity", "Edit image cancelled by user") } else { Log.w("ComposeActivity", "Edit image failed: " + result.error) - displayTransientError(R.string.error_media_edit_failed) + displayTransientError(R.string.error_image_edit_failed) } viewModel.cropImageItemOld = null } diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 49f3912ed..4492e3ea4 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -534,7 +534,6 @@ تاسکی برای استفاده از آگاهی‌های ارسالی با UnifiedPush نیاز به اجازهٔ اشتراک آگاهی‌ها روی کارساز ماستودنتان دارد. این کار نیازمند ورود دوباره برای تغییر حوزه‌های OAuth اعطایی به تاسکی است. استفاده از گزینهٔ ورود دوباره در این‌جا یا در ترجیحات حساب، تمامی انباره‌ها و پیش‌نویس‌های محلیتان را نگه خواهد داشت. نتوانست صفحهٔ ورود را بار کند. کسی ثبت‌نام کرد - پیوست نتوانست ویرایش شود. ویرایش‌های فرسته ویرایش تصویر %s فرسته‌اش را ویراست diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml index 992972899..294bdc56a 100644 --- a/app/src/main/res/values-gd/strings.xml +++ b/app/src/main/res/values-gd/strings.xml @@ -562,6 +562,5 @@ Clàraich a-steach às ùr leis a h-uile cunntas a chur na taice ri brathan putaidh an comas. Clàraich a-steach às ùr airson brathan putaidh 1+ - Cha deach le deasachadh a’ cheanglachain. Deasaich an dealbh \ No newline at end of file diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 1d2c75f05..af350478c 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -536,6 +536,5 @@ Non se puido cargar a páxina de inicio. Gardando borrador… 1+ - Non se puido editar o anexo. Editar imaxe \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 1afc2e296..262ce0e04 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -542,7 +542,6 @@ 1+ Nem tudtuk betölteni a bejelentkező oldalt. Vázlat mentése… - A csatolmány nem szerkeszthető. Ahhoz, hogy használhass leküldési értesítéseket a UnifiedPush szolgáltatással, a Tusky-nak fel kell iratkoznia az értesítésekre a Mastodon szervereden. Ehhez új bejelentkezésre van szükség, hogy a Tusky számára kiosztott OAuth jogosultságok megváltozzanak. Az újbóli bejelentkezés funkció használata itt vagy a Fiókbeállításoknál meg fogja őrizni a helyi piszkozataidat és a cache tartalmát. Újra bejelentkeztél a fiókodba, hogy feliratkoztasd a Tusky-t a leküldési értesítések használatára. Ugyanakkor vannak még fiókjaid, melyek még nem lettek így migrálva. Válts át rájuk és jelentkezz be újra mindegyikben, hogy ezekben is engedélyezd a UnifiedPush értesítések támogatását. Kép szerkesztése diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 57ca37936..77350a872 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -532,7 +532,6 @@ Skrá inn 1+ Gat ekki lesið innskráningarsíðuna. - Ekki var hægt að breyta viðhenginu. Breyta mynd Vista drög… Skráðu aftur inn fyrir ýti-tilkynningar diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 5a1de3a5f..952808e68 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -540,7 +540,6 @@ Modifiche ai post Notifiche di quando i post con cui hai interagito vengono modificati Non è stato possibile caricare la pagina di login. - L\'allegato non può essere modificato. Modifica immagine Salvataggio bozza… Scartare diff --git a/app/src/main/res/values-no-rNB/strings.xml b/app/src/main/res/values-no-rNB/strings.xml index 37fda7652..6b34f63ac 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -536,6 +536,5 @@ Du har logget inn på nytt for å tillate Tusky til å sende pushvarsler, men du har fortsatt andre konti som ikke har fått den nødvendige tillatelsen. Bytt til dem og logg inn på nytt på samme måte for å skru på støtte for pushvarsler via UnifiedPush. Lagrer kladd… 1+ - Vedlegget kan ikke redigeres. Rediger bilde \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 60af4f09e..3117e713d 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -559,5 +559,4 @@ Приєднується %1$s Редагувати зображення 1+ - Неможливо редагувати вкладення. \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 5522d9964..db11b4558 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -525,6 +525,5 @@ Bạn đã đăng nhập lại vào tài khoản hiện tại của mình để cấp quyền thông báo đẩy cho Tusky. Tuy nhiên, bạn vẫn có các tài khoản khác chưa kích hoạt thông báo đẩy theo cách này. Chuyển sang chúng và đăng nhập từng cái một để cho phép hỗ trợ thông báo UnifiedPush. Để sử dụng thông báo đẩy qua UnifiedPush, Tusky cần có quyền đăng ký thông báo trên máy chủ Mastodon của bạn. Bạn hãy thoát ra rồi đăng nhập lại để thay đổi phạm vi OAuth được cấp cho Tusky. Sử dụng đăng nhập lại ở đây hoặc trong cài đặt Tài khoản sẽ bảo toàn tất cả các tút nháp và bộ nhớ đệm trên điện thoại của bạn. 1+ - Tập tin đính kèm không thể chỉnh sửa. Sửa ảnh \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 6a8223629..818b064d0 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -544,6 +544,5 @@ 重新登录所有账户来启用推送通知支持。 为了通过 UnifiedPush 使用推送通知,Tusky 需要订阅你 Mastodon 服务器通知的权限。这需要重新登录来更改授予 Tusky 的 OAuth 作用域。使用此处或账户首选项中“重新登录”选项将保留你所有的本地草稿和缓存。 1+ - 无法编辑附件。 编辑图片 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ad87b6fc8..7a9d1b20d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,7 +14,7 @@ The file must be less than 8MB. Video files must be less than 40MB. Audio files must be less than 40MB. - The attachment could not be edited. + The image could not be edited. That type of file cannot be uploaded. That file could not be opened. Permission to read media is required. From 8551785389a4e98f9f36dbb825f5f8d0eabbec98 Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Fri, 24 Jun 2022 21:47:49 +0200 Subject: [PATCH 49/82] Fix hiding/showing preview cards for sensitive statuses (#2600) * Update comment on StatusViewData.isCollapsible * Fix hiding/showing preview cards for sensitive statuses. Fixes #2565 * Fix typo --- .../tusky/adapter/StatusBaseViewHolder.java | 1 + .../adapter/StatusDetailedViewHolder.java | 18 ++++++++++++------ .../tusky/fragment/NotificationsFragment.java | 2 +- .../tusky/fragment/ViewThreadFragment.java | 2 +- .../tusky/viewdata/StatusViewData.kt | 6 +++--- 5 files changed, 18 insertions(+), 11 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 8b7d6eecd..980f644b0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -1046,6 +1046,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { actionable.getPoll() == null && card != null && !TextUtils.isEmpty(card.getUrl()) && + (!actionable.getSensitive() || status.isExpanded()) && (!status.isCollapsible() || !status.isCollapsed())) { cardView.setVisibility(View.VISIBLE); cardTitle.setText(card.getTitle()); 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 bf2c05e01..ae0b0678b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -103,20 +103,26 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder { @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 + // We never collapse statuses in the detail view + StatusViewData.Concrete uncollapsedStatus = (status.isCollapsible() && status.isCollapsed()) ? + status.copyWithCollapsed(false) : + status; + + super.setupWithStatus(uncollapsedStatus, listener, statusDisplayOptions, payloads); + setupCard(uncollapsedStatus, CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status if (payloads == null) { + Status actionable = uncollapsedStatus.getActionable(); if (!statusDisplayOptions.hideStats()) { - setReblogAndFavCount(status.getActionable().getReblogsCount(), - status.getActionable().getFavouritesCount(), listener); + setReblogAndFavCount(actionable.getReblogsCount(), + actionable.getFavouritesCount(), listener); } else { hideQuantitativeStats(); } - setApplication(status.getActionable().getApplication()); + setApplication(actionable.getApplication()); - setStatusVisibility(status.getActionable().getVisibility()); + setStatusVisibility(actionable.getVisibility()); } } 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 aed121a17..ee5402860 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -538,7 +538,7 @@ public class NotificationsFragment extends SFragment implements @Override public void onContentCollapsedChange(boolean isCollapsed, int position) { - updateViewDataAt(position, (vd) -> vd.copyWIthCollapsed(isCollapsed)); + updateViewDataAt(position, (vd) -> vd.copyWithCollapsed(isCollapsed)); ; } diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java index 98466cc99..1d6525d18 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java @@ -387,7 +387,7 @@ public final class ViewThreadFragment extends SFragment implements public void onContentCollapsedChange(boolean isCollapsed, int position) { adapter.setItem( position, - statuses.getPairedItem(position).copyWIthCollapsed(isCollapsed), + statuses.getPairedItem(position).copyWithCollapsed(isCollapsed), true ); } 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 8ac212d90..bef7d0e1d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt @@ -47,8 +47,8 @@ sealed class StatusViewData { get() = status.id /** - * Specifies whether the content of this post is allowed to be collapsed or if it should show - * all content regardless. + * Specifies whether the content of this post is long enough to be automatically + * collapsed or if it should show all content regardless. * * @return Whether the post is collapsible or never collapsed. */ @@ -106,7 +106,7 @@ sealed class StatusViewData { } /** Helper for Java */ - fun copyWIthCollapsed(isCollapsed: Boolean): Concrete { + fun copyWithCollapsed(isCollapsed: Boolean): Concrete { return copy(isCollapsed = isCollapsed) } } From de358e530773da917a242512f432b1901388254e Mon Sep 17 00:00:00 2001 From: codl Date: Sun, 26 Jun 2022 08:59:49 +0000 Subject: [PATCH 50/82] Translated using Weblate (French) Currently translated at 100.0% (488 of 488 strings) Translated using Weblate (French) Currently translated at 100.0% (488 of 488 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 | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 1bcd01ce5..823e42851 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -264,7 +264,7 @@ Média Réponse à @%s en charger plus - Timelines publiques + Fils publics Conversations Ajouter un filtre Modifier un filtre @@ -550,4 +550,14 @@ Ici depuis %1$s Détails Sauvegarde du brouillon … + >1 + Tusky peut maintenant recevoir les notifications instantanées de ce compte. Cependant, d\'autres de vos comptes n\'ont pas encore accès aux notifications instantanées. Basculez sur chacun de vos comptes et reconnectez les afin de recevoir les notifications avec UnifiedPush. + La page de connexion n\'a pas pu être chargée. + Retoucher l\'image + Le fichier n\'a pas pu être retouché. + Fermer + Se reconnecter pour recevoir les notifications instantanées + Afin de recevoir les notifications via UnifiedPush, Tusky doit demander à votre serveur Mastodon la permission de s\'inscrire aux notifications. Ceci nécessite une reconnexion de vos comptes afin de changer les droits OAuth accordés a Tusky. En utilisant l\'option de reconnexion ici ou dans les préférences de compte, vos brouillons et le cache seront préservés. + Reconnectez tous vos comptes pour activer les notifications instantanées. + L\'image n\'a pas pu être retouchée. \ No newline at end of file From d0adfbd6073a9721344cc3d932ba20df2b151e30 Mon Sep 17 00:00:00 2001 From: Weblate Date: Sun, 26 Jun 2022 08:59:49 +0000 Subject: [PATCH 51/82] Update translation files Updated by "Cleanup translation files" hook in Weblate. Co-authored-by: Weblate Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/ Translation: Tusky/Tusky --- app/src/main/res/values-fr/strings.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 823e42851..8baa4ddfc 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -554,7 +554,6 @@ Tusky peut maintenant recevoir les notifications instantanées de ce compte. Cependant, d\'autres de vos comptes n\'ont pas encore accès aux notifications instantanées. Basculez sur chacun de vos comptes et reconnectez les afin de recevoir les notifications avec UnifiedPush. La page de connexion n\'a pas pu être chargée. Retoucher l\'image - Le fichier n\'a pas pu être retouché. Fermer Se reconnecter pour recevoir les notifications instantanées Afin de recevoir les notifications via UnifiedPush, Tusky doit demander à votre serveur Mastodon la permission de s\'inscrire aux notifications. Ceci nécessite une reconnexion de vos comptes afin de changer les droits OAuth accordés a Tusky. En utilisant l\'option de reconnexion ici ou dans les préférences de compte, vos brouillons et le cache seront préservés. From aba31be49ad88b75963030fc6c918557d0510dac Mon Sep 17 00:00:00 2001 From: Stefano Pigozzi Date: Sun, 26 Jun 2022 08:59:49 +0000 Subject: [PATCH 52/82] Translated using Weblate (Italian) Currently translated at 98.5% (481 of 488 strings) Co-authored-by: Stefano Pigozzi Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/it/ Translation: Tusky/Tusky --- app/src/main/res/values-it/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 952808e68..c6af80f89 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -488,7 +488,7 @@ mi viene richiesto di seguirmi Nascondi statistiche quantitative sui profili Nascondi le statistiche quantitative sui post - Limita le notifiche dalla timeline + Limita notifiche riguardo statistiche quantitative Rivedi le notifiche Benessere Notifiche di nuovi post di qualcuno a cui sei iscritto From 8bccc96a5fd5ae8bc2f080ee8fe5feb88013eea7 Mon Sep 17 00:00:00 2001 From: Eric Date: Sun, 26 Jun 2022 08:59:49 +0000 Subject: [PATCH 53/82] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (488 of 488 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 818b064d0..3ca2c1d55 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -545,4 +545,5 @@ 为了通过 UnifiedPush 使用推送通知,Tusky 需要订阅你 Mastodon 服务器通知的权限。这需要重新登录来更改授予 Tusky 的 OAuth 作用域。使用此处或账户首选项中“重新登录”选项将保留你所有的本地草稿和缓存。 1+ 编辑图片 + 无法编辑图片。 \ No newline at end of file From ea0436d2f58a67297cfc22b69baa4bfe4c6fd3eb Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Sun, 26 Jun 2022 08:59:49 +0000 Subject: [PATCH 54/82] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (488 of 488 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 6b34f63ac..1ac18f3a5 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -537,4 +537,5 @@ Lagrer kladd… 1+ Rediger bilde + Bildet kunne ikke redigeres. \ No newline at end of file From 6c9f1f1563ba652eecb5e3d2d150e363437eae64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Sun, 26 Jun 2022 08:59:49 +0000 Subject: [PATCH 55/82] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (488 of 488 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 db11b4558..024741bff 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -526,4 +526,5 @@ Để sử dụng thông báo đẩy qua UnifiedPush, Tusky cần có quyền đăng ký thông báo trên máy chủ Mastodon của bạn. Bạn hãy thoát ra rồi đăng nhập lại để thay đổi phạm vi OAuth được cấp cho Tusky. Sử dụng đăng nhập lại ở đây hoặc trong cài đặt Tài khoản sẽ bảo toàn tất cả các tút nháp và bộ nhớ đệm trên điện thoại của bạn. 1+ Sửa ảnh + Hình ảnh này không thể sửa. \ No newline at end of file From d9a1c55ee40de906e0168ca8a3de360acc06d34f Mon Sep 17 00:00:00 2001 From: "Gera, Zoltan" Date: Sun, 26 Jun 2022 08:59:49 +0000 Subject: [PATCH 56/82] Translated using Weblate (Hungarian) Currently translated at 100.0% (488 of 488 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 | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 262ce0e04..d133c3ac8 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -545,4 +545,5 @@ Ahhoz, hogy használhass leküldési értesítéseket a UnifiedPush szolgáltatással, a Tusky-nak fel kell iratkoznia az értesítésekre a Mastodon szervereden. Ehhez új bejelentkezésre van szükség, hogy a Tusky számára kiosztott OAuth jogosultságok megváltozzanak. Az újbóli bejelentkezés funkció használata itt vagy a Fiókbeállításoknál meg fogja őrizni a helyi piszkozataidat és a cache tartalmát. Újra bejelentkeztél a fiókodba, hogy feliratkoztasd a Tusky-t a leküldési értesítések használatára. Ugyanakkor vannak még fiókjaid, melyek még nem lettek így migrálva. Válts át rájuk és jelentkezz be újra mindegyikben, hogy ezekben is engedélyezd a UnifiedPush értesítések támogatását. Kép szerkesztése + A kép nem szerkeszthető. \ No newline at end of file From 69d8dd9662b091aad3220bdb029a9c273dd70557 Mon Sep 17 00:00:00 2001 From: ButterflyOfFire Date: Sun, 26 Jun 2022 08:59:49 +0000 Subject: [PATCH 57/82] Translated using Weblate (French) Currently translated at 100.0% (488 of 488 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 | 68 ++++++++++++-------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 8baa4ddfc..5025ae1d2 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -27,7 +27,7 @@ Onglets Fil Messages - Pouets & réponses + Avec réponses Épinglés Abonnements Abonné·e·s @@ -42,7 +42,7 @@ %s a partagé Contenu sensible Média caché - Cliquer pour voir + Appuyer pour voir Voir plus Voir moins Déplier @@ -156,7 +156,7 @@ Télécharger Révoquer la demande d’abonnement ? Ne plus suivre ce compte ? - Supprimer ce pouet ? + Supprimer ce message \? Public : afficher dans les fils publics Non listé : ne pas afficher dans les fils publics Abonné·e·s uniquement : seul·e·s vos abonné·e·s verront vos statuts @@ -202,7 +202,7 @@ Public Non listé Abonné·e·s uniquement - Taille du texte pour les statuts + Taille du texte des messages Plus petit Petit Moyen @@ -243,8 +243,8 @@ https://github.com/tuskyapp/Tusky/issues Profil de Tusky - Partager le contenu du pouet - Partager le lien du pouet + Partager le contenu du message + Partager le lien du message Images Vidéo Demande d’abonnement effectuée @@ -296,19 +296,19 @@ Verrouiller le compte Vous devez approuver manuellement les abonnements Enregistrer comme brouillon ? - Envoi du pouet… - Erreur lors de l’envoi du pouet - Envoi des pouets + Envoi du message… + Erreur lors de l’envoi du message + Envoi des messages Envoi annulé - Une copie du pouet a été sauvegardée dans vos brouillons + Une copie du message a été sauvegardée dans vos brouillons Écrire Votre instance %s n’a pas d’émojis personnalisés Style d’émojis Par défaut du système Vous devez commencer par télécharger ces jeux d’émojis Recherche en cours… - Déplier/replier tout les statuts - Ouvrir le pouet + Déplier/replier tout les messages + Ouvrir le message Un redémarrage de l’application est nécessaire Vous devrez redémarrer Tusky pour appliquer ces modifications Plus tard @@ -350,16 +350,12 @@ maximum de %1$d onglet atteint maximum de %1$d onglets atteint - Média : %s - + Média : %s Avertissement : %s - Pas de description - - Reblogué - - Mis en favoris - + Aucune description + Partagé + Mis en favoris Public Non listé @@ -377,7 +373,7 @@ Afficher l\'indicateur de robots Désirez-vous nettoyer toutes vos notifications de façon permanente \? Effacer et ré-écrire - Effacer et ré-écrire ce pouet \? + Effacer et ré-écrire ce message \? Termina à %s Terminé Voter @@ -390,15 +386,15 @@ %d jours restants - %d heure restant + %d heure restante %d heures restantes - %d minute restant + %d minute restante %d minutes restantes - %d seconde restant + %d seconde restante %d secondes restantes Activer l’animation des avatars @@ -418,7 +414,7 @@ Commentaires additionnels Transférer à %s Échec du signalement - Échec de récupération des statuts + Échec de récupération des messages Le rapport sera envoyé aux modérateur·rice·s de votre instance. Vous pouvez expliquer pourquoi vous signalez le compte ci-dessous : Êtes-vous sûr⋅e de vouloir bloquer %s en entier \? Vous ne verrez plus de contenu provenant de ce domaine, ni dans les fils publics, ni dans vos notifications. Vos abonné·e·s utilisant ce domaine seront retiré·e·s. Terminé @@ -444,8 +440,8 @@ Éditer Pouets planifiés Éditer - Pouets programmés - Planifier le pouet + Messages programmés + Planifier le message Réinitialiser Erreur lors de la recherche du post %s Propulsé par Tusky @@ -457,7 +453,7 @@ Liste Les fichiers audio doivent avoir moins de 40 Mo. Vous n’avez aucun brouillon. - Vous n’avez aucun pouet planifié. + Vous n’avez aucun message planifié. L’intervalle minimum de planification sur Mastodon est de 5 minutes. Demandes d\'abonnement Bloquer @%s \? @@ -528,7 +524,7 @@ Audio Demander confirmation avant de mettre en favoris Le message auquel répondait ce brouillon a été supprimé - Échec d’envoi du pouet ! + Échec d’envoi du message ! Bien que votre compte ne soit pas verrouillé, l’équipe de %1$s a pensé que vous voudriez valider manuellement les demandes de d’abonnement provenant de ces comptes. Échec du chargement des informations de réponse 30 jours @@ -540,23 +536,23 @@ Rédiger un message %s a créé un compte Nouveaux comptes - Notifications quand quelqu\'un crée un nouveau compte + 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é + un message avec lequel j’ai interagi est modifié Messages modifiés - Notifications quand un post avec lequel vous avez interagi est modifié + Notifications quand un message avec lequel vous avez interagi est modifié Se connecter Ici depuis %1$s Détails Sauvegarde du brouillon … >1 Tusky peut maintenant recevoir les notifications instantanées de ce compte. Cependant, d\'autres de vos comptes n\'ont pas encore accès aux notifications instantanées. Basculez sur chacun de vos comptes et reconnectez les afin de recevoir les notifications avec UnifiedPush. - La page de connexion n\'a pas pu être chargée. - Retoucher l\'image + La page de connexion ne peut être chargée. + Retoucher l’image Fermer Se reconnecter pour recevoir les notifications instantanées - Afin de recevoir les notifications via UnifiedPush, Tusky doit demander à votre serveur Mastodon la permission de s\'inscrire aux notifications. Ceci nécessite une reconnexion de vos comptes afin de changer les droits OAuth accordés a Tusky. En utilisant l\'option de reconnexion ici ou dans les préférences de compte, vos brouillons et le cache seront préservés. + Afin de recevoir les notifications via UnifiedPush, Tusky doit demander à votre serveur Mastodon la permission de s’inscrire aux notifications. Ceci nécessite une reconnexion de vos comptes afin de changer les droits OAuth accordés a Tusky. En utilisant l’option de reconnexion ici ou dans les préférences de compte, vos brouillons et le cache seront préservés. Reconnectez tous vos comptes pour activer les notifications instantanées. - L\'image n\'a pas pu être retouchée. + L\'image n’a pas pu être retouchée. \ No newline at end of file From 9dcb1b666c7fc71753dd4309d8cb1f8df0ca16d3 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 30 Jun 2022 20:49:27 +0200 Subject: [PATCH 58/82] show push migration snackbar above floating action button (#2598) --- .../java/com/keylesspalace/tusky/MainActivity.kt | 2 +- .../notifications/PushNotificationHelper.kt | 15 ++++++++++----- app/src/main/res/layout/activity_main.xml | 1 + 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt index fd2e15b73..24e0c4020 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -703,7 +703,7 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidInje NotificationHelper.createNotificationChannelsForAccount(accountManager.activeAccount!!, this) // Setup push notifications - showMigrationNoticeIfNecessary(this, binding.root, accountManager) + showMigrationNoticeIfNecessary(this, binding.mainCoordinatorLayout, binding.composeButton, accountManager) if (NotificationHelper.areNotificationsEnabled(this, accountManager)) { lifecycleScope.launch { enablePushNotificationsWithFallback(this@MainActivity, mastodonApi, accountManager) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt index f1b714301..68a443027 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt @@ -51,7 +51,12 @@ private fun accountNeedsMigration(account: AccountEntity): Boolean = fun currentAccountNeedsMigration(accountManager: AccountManager): Boolean = accountManager.activeAccount?.let(::accountNeedsMigration) ?: false -fun showMigrationNoticeIfNecessary(context: Context, parent: View, accountManager: AccountManager) { +fun showMigrationNoticeIfNecessary( + context: Context, + parent: View, + anchorView: View?, + accountManager: AccountManager +) { // No point showing anything if we cannot enable it if (!isUnifiedPushAvailable(context)) return if (!anyAccountNeedsMigration(accountManager)) return @@ -59,10 +64,10 @@ fun showMigrationNoticeIfNecessary(context: Context, parent: View, accountManage val pm = PreferenceManager.getDefaultSharedPreferences(context) if (pm.getBoolean(KEY_MIGRATION_NOTICE_DISMISSED, false)) return - Snackbar.make(parent, R.string.tips_push_notification_migration, Snackbar.LENGTH_INDEFINITE).apply { - setAction(R.string.action_details) { showMigrationExplanationDialog(context, accountManager) } - show() - } + Snackbar.make(parent, R.string.tips_push_notification_migration, Snackbar.LENGTH_INDEFINITE) + .setAnchorView(anchorView) + .setAction(R.string.action_details) { showMigrationExplanationDialog(context, accountManager) } + .show() } private fun showMigrationExplanationDialog(context: Context, accountManager: AccountManager) { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2f8777092..cc9f95704 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -8,6 +8,7 @@ android:fitsSystemWindows="true"> From 8a0848d2529c946d20ab5b7047e0e26616cd782a Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 30 Jun 2022 20:49:48 +0200 Subject: [PATCH 59/82] fix data loss when re-adding existing account (#2601) --- .../tusky/components/login/LoginActivity.kt | 53 ++++++++++++------ .../keylesspalace/tusky/db/AccountManager.kt | 54 ++++++++++--------- .../tusky/network/MastodonApi.kt | 5 +- app/src/main/res/values/strings.xml | 1 + 4 files changed, 71 insertions(+), 42 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 e55bbd71f..68fc5233d 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 @@ -34,6 +34,7 @@ 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.AccessToken import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.getNonNullString import com.keylesspalace.tusky.util.rickRoll @@ -236,32 +237,50 @@ class LoginActivity : BaseActivity(), Injectable { domain, clientId, clientSecret, oauthRedirectUri, code, "authorization_code" ).fold( { accessToken -> - accountManager.addAccount( - accessToken = accessToken.accessToken, - domain = domain, - clientId = clientId, - clientSecret = clientSecret, - oauthScopes = OAUTH_SCOPES - ) - - 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) + fetchAccountDetails(accessToken, domain, clientId, clientSecret) }, { 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), - ) + Log.e(TAG, getString(R.string.error_retrieving_oauth_token), e) } ) } + private suspend fun fetchAccountDetails( + accessToken: AccessToken, + domain: String, + clientId: String, + clientSecret: String + ) { + + mastodonApi.accountVerifyCredentials( + domain = domain, + auth = "Bearer ${accessToken.accessToken}" + ).fold({ newAccount -> + accountManager.addAccount( + accessToken = accessToken.accessToken, + domain = domain, + clientId = clientId, + clientSecret = clientSecret, + oauthScopes = OAUTH_SCOPES, + newAccount = newAccount + ) + + 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_loading_account_details) + Log.e(TAG, getString(R.string.error_loading_account_details), e) + }) + } + private fun setLoading(loadingState: Boolean) { if (loadingState) { binding.loginLoadingLayout.visibility = View.VISIBLE diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt index 6048c855e..04f8e6f56 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt @@ -48,18 +48,21 @@ class AccountManager @Inject constructor(db: AppDatabase) { } /** - * Adds a new empty account and makes it the active account. - * More account information has to be added later with [updateActiveAccount] - * or the account wont be saved to the database. + * Adds a new account and makes it the active account. * @param accessToken the access token for the new account * @param domain the domain of the accounts Mastodon instance + * @param clientId the oauth client id used to sign in the account + * @param clientSecret the oauth client secret used to sign in the account + * @param oauthScopes the oauth scopes granted to the account + * @param newAccount the [Account] as returned by the Mastodon Api */ fun addAccount( accessToken: String, domain: String, clientId: String, clientSecret: String, - oauthScopes: String + oauthScopes: String, + newAccount: Account ) { activeAccount?.let { @@ -68,18 +71,31 @@ class AccountManager @Inject constructor(db: AppDatabase) { accountDao.insertOrReplace(it) } - - val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0 - val newAccountId = maxAccountId + 1 - activeAccount = AccountEntity( - id = newAccountId, - domain = domain.lowercase(Locale.ROOT), + // check if this is a relogin with an existing account, if yes update it, otherwise create a new one + val newAccountEntity = accounts.find { account -> + domain == account.domain && newAccount.id == account.accountId + }?.copy( accessToken = accessToken, clientId = clientId, clientSecret = clientSecret, - oauthScopes = oauthScopes, - isActive = true - ) + oauthScopes = oauthScopes + ) ?: run { + val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0 + val newAccountId = maxAccountId + 1 + AccountEntity( + id = newAccountId, + domain = domain.lowercase(Locale.ROOT), + accessToken = accessToken, + clientId = clientId, + clientSecret = clientSecret, + oauthScopes = oauthScopes, + isActive = true, + accountId = newAccount.id + ).also { accounts.add(it) } + } + + activeAccount = newAccountEntity + updateActiveAccount(newAccount) } /** @@ -135,17 +151,7 @@ class AccountManager @Inject constructor(db: AppDatabase) { it.emojis = account.emojis ?: emptyList() Log.d(TAG, "updateActiveAccount: saving account with id " + it.id) - it.id = accountDao.insertOrReplace(it) - - val accountIndex = accounts.indexOf(it) - - if (accountIndex != -1) { - // in case the user was already logged in with this account, remove the old information - accounts.removeAt(accountIndex) - accounts.add(accountIndex, it) - } else { - accounts.add(it) - } + accountDao.insertOrReplace(it) } } 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 b8834a54b..e1d18e9f6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -250,7 +250,10 @@ interface MastodonApi { ): NetworkResult @GET("api/v1/accounts/verify_credentials") - suspend fun accountVerifyCredentials(): NetworkResult + suspend fun accountVerifyCredentials( + @Header(DOMAIN_HEADER) domain: String? = null, + @Header("Authorization") auth: String? = null, + ): NetworkResult @FormUrlEncoded @PATCH("api/v1/accounts/update_credentials") diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7a9d1b20d..1a7ef2983 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. + Failed loading account details Could not load the login page. The post is too long! The file must be less than 8MB. From 519501e7470cf0e03088910b8d60531450e51b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Wed, 29 Jun 2022 19:26:51 +0000 Subject: [PATCH 60/82] Translated using Weblate (Vietnamese) Currently translated at 100.0% (18 of 18 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/vi/ --- fastlane/metadata/android/vi/changelogs/93.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 fastlane/metadata/android/vi/changelogs/93.txt diff --git a/fastlane/metadata/android/vi/changelogs/93.txt b/fastlane/metadata/android/vi/changelogs/93.txt new file mode 100644 index 000000000..b3f8aa270 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/93.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Hỗ trợ Unified Push. Bạn cần đăng nhập lại để sử dụng được. +- Hiện số lượng trả lời trên nút +- Cắt ảnh khi viết tút +- Hiện ngày tham gia Mastodon +- Khi xem danh sách, tựa đề sẽ hiện trên toolbar +- Sửa lỗi vặt +- Cải thiện bản dịch From fc1562df960d18abf783e99f3c38f4527a455bf0 Mon Sep 17 00:00:00 2001 From: XoseM Date: Wed, 29 Jun 2022 19:26:51 +0000 Subject: [PATCH 61/82] Translated using Weblate (Galician) Currently translated at 100.0% (18 of 18 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/gl/ --- fastlane/metadata/android/gl/changelogs/93.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 fastlane/metadata/android/gl/changelogs/93.txt diff --git a/fastlane/metadata/android/gl/changelogs/93.txt b/fastlane/metadata/android/gl/changelogs/93.txt new file mode 100644 index 000000000..0e4befb4d --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/93.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Soporte para Unified Push. Para activar a función tes que volver a acceder ás túas contas. +- Agora aparece nas cronoloxías o número de respostas a unha publicación. +- Podes recortar as imaxes cando escribes unha publicación. +- Os perfís mostran a data na que foron creados. +- Móstrase o título da lista na barra de ferramentas ao visualizala. +- Arranxamos moitos fallos. +- Melloras nas traducións. From 202eae23ab2534a7b67a124ec100af869522e2a0 Mon Sep 17 00:00:00 2001 From: codl Date: Wed, 29 Jun 2022 19:26:51 +0000 Subject: [PATCH 62/82] Translated using Weblate (French) Currently translated at 100.0% (18 of 18 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/fr/ --- fastlane/metadata/android/fr/changelogs/91.txt | 6 ++++++ fastlane/metadata/android/fr/changelogs/93.txt | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 fastlane/metadata/android/fr/changelogs/91.txt create mode 100644 fastlane/metadata/android/fr/changelogs/93.txt diff --git a/fastlane/metadata/android/fr/changelogs/91.txt b/fastlane/metadata/android/fr/changelogs/91.txt new file mode 100644 index 000000000..e385800dc --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Les nouveaux types de notifications de Mastodon 3.5 sont maintenant supportés +- Le badge robot est maintenant plus joli et s'adapte au thème choisi +- Il est maintenant possible de sélectionner le texte dans l'écran de détails d'un post +- Beaucoup de bogues résolus, dont un qui empêchait de se connecter sous Android 6 ou inférieur diff --git a/fastlane/metadata/android/fr/changelogs/93.txt b/fastlane/metadata/android/fr/changelogs/93.txt new file mode 100644 index 000000000..15cbac96a --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/93.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Les notifications via UnifiedPush sont à présent supportées. Pour les activer vous devrez reconnecter vos comptes. +- Le nombre de réponses est maintenant affiché sur chaque post dans les fils. +- Les images peuvent maintenant être rognées lors de l'écriture d'un message. +- Les profils affichent à présent leur date de création. +- Lorsqu'une liste est affichée, son nom apparaît maintenant dans la barre d'outils. +- Beaucoup de bogues résolus. +- Des améliorations sur les traductions. From 286cf62f4f8a8f755ef2ba910ff4109e4335da10 Mon Sep 17 00:00:00 2001 From: "Gera, Zoltan" Date: Wed, 29 Jun 2022 19:26:51 +0000 Subject: [PATCH 63/82] Translated using Weblate (Hungarian) Currently translated at 100.0% (18 of 18 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/hu/ --- fastlane/metadata/android/hu/changelogs/93.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 fastlane/metadata/android/hu/changelogs/93.txt diff --git a/fastlane/metadata/android/hu/changelogs/93.txt b/fastlane/metadata/android/hu/changelogs/93.txt new file mode 100644 index 000000000..fe40fdf1f --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/93.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Egységes leküldés (Unified Push) támogatása. A támogatás aktiválásához újra jelentkezz be a fiókjaidba. +- A bejegyzésekre érkezett válaszok száma már látható az idővonalon. +- Bejegyzés szerkesztése közben meg lehet vágni a képeket. +- A profilokon látható ezek létrehozásának időpontja. +- Lista megtekintésekor ennek címe látható az eszköztáron. +- Rengeteg hibajavítás +- Fordítási javítások From 0e2eefdb942de5c2a666863d5f7fde007a6e2b24 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Wed, 29 Jun 2022 19:26:51 +0000 Subject: [PATCH 64/82] Translated using Weblate (Ukrainian) Currently translated at 100.0% (18 of 18 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/uk/ --- fastlane/metadata/android/uk/changelogs/93.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 fastlane/metadata/android/uk/changelogs/93.txt diff --git a/fastlane/metadata/android/uk/changelogs/93.txt b/fastlane/metadata/android/uk/changelogs/93.txt new file mode 100644 index 000000000..a31309fdc --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/93.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Підтримка Unified Push. Щоб активувати підтримку, вам потрібно повторно увійти в обліковий запис. +- Кількість відповідей на допис тепер вказана у стрічках. +- Зображення тепер можуть обрізатися під час складання допису. +- Профілі тепер показують дату їхнього створення. +- Під час перегляду списку назва відтепер показана на панелі інструментів. +- Усунення помилок +- Покращення перекладу From da082b8ef76371d11e52b35496791ee7057beeb9 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Wed, 29 Jun 2022 19:26:51 +0000 Subject: [PATCH 65/82] Translated using Weblate (Ukrainian) Currently translated at 100.0% (488 of 488 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 3117e713d..489f872ab 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -559,4 +559,5 @@ Приєднується %1$s Редагувати зображення 1+ + Неможливо редагувати зображення. \ No newline at end of file From 62c4cfde89ea8aec3ecf0a7871ca5304536ef6b2 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Thu, 30 Jun 2022 20:51:05 +0200 Subject: [PATCH 66/82] improve media upload error messages (#2602) --- .../components/compose/ComposeActivity.kt | 26 ++++++++++++------- .../components/compose/ComposeViewModel.kt | 2 +- .../tusky/components/compose/MediaUploader.kt | 18 ++++++++++--- .../tusky/util/ThrowableExtensions.kt | 26 +++++++++++++++++++ .../tusky/viewmodel/EditProfileViewModel.kt | 20 ++------------ app/src/main/res/layout/activity_compose.xml | 1 + 6 files changed, 61 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/com/keylesspalace/tusky/util/ThrowableExtensions.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 1d4703406..370f89c35 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 @@ -161,7 +161,7 @@ class ComposeActivity : val uriNew = result.uriContent if (result.isSuccessful && uriNew != null) { viewModel.cropImageItemOld?.let { itemOld -> - val size = getMediaSize(getApplicationContext().getContentResolver(), uriNew) + val size = getMediaSize(contentResolver, uriNew) lifecycleScope.launch { viewModel.addMediaToQueue( @@ -407,8 +407,13 @@ class ComposeActivity : enableButton(binding.composeAddMediaButton, active, active) enablePollButton(media.isNullOrEmpty()) }.subscribe() - viewModel.uploadError.observe { - displayTransientError(R.string.error_media_upload_sending) + viewModel.uploadError.observe { throwable -> + Log.w(TAG, "media upload failed", throwable) + if (throwable is UploadServerError) { + displayTransientError(throwable.errorMessage) + } else { + displayTransientError(R.string.error_media_upload_sending) + } } viewModel.setupComplete.observe { // Focus may have changed during view model setup, ensure initial focus is on the edit field @@ -553,19 +558,23 @@ class ComposeActivity : super.onSaveInstanceState(outState) } - private fun displayTransientError(@StringRes stringId: Int) { - val bar = Snackbar.make(binding.activityCompose, stringId, Snackbar.LENGTH_LONG) + private fun displayTransientError(errorMessage: String) { + val bar = Snackbar.make(binding.activityCompose, errorMessage, Snackbar.LENGTH_LONG) // necessary so snackbar is shown over everything bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) + bar.setAnchorView(R.id.composeBottomBar) bar.show() } + private fun displayTransientError(@StringRes stringId: Int) { + displayTransientError(getString(stringId)) + } private fun toggleHideMedia() { this.viewModel.toggleMarkSensitive() } private fun updateSensitiveMediaToggle(markMediaSensitive: Boolean, contentWarningShown: Boolean) { - if (viewModel.media.value.isNullOrEmpty()) { + if (viewModel.media.value.isEmpty()) { binding.composeHideMediaButton.hide() } else { binding.composeHideMediaButton.show() @@ -904,11 +913,10 @@ class ComposeActivity : // Currently the only supported lossless format is png. val mimeType: String? = contentResolver.getType(item.uri) val isPng: Boolean = mimeType != null && mimeType.endsWith("/png") - val context = getApplicationContext() - val tempFile = createNewImageFile(context, if (isPng) ".png" else ".jpg") + val tempFile = createNewImageFile(this, if (isPng) ".png" else ".jpg") // "Authority" must be the same as the android:authorities string in AndroidManifest.xml - val uriNew = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", tempFile) + val uriNew = FileProvider.getUriForFile(this, BuildConfig.APPLICATION_ID + ".fileprovider", tempFile) viewModel.cropImageItemOld = item 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 f15565d03..a7e1779cf 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 @@ -219,7 +219,7 @@ class ComposeViewModel @Inject constructor( val contentWarningChanged = showContentWarning.value!! && !contentWarning.isNullOrEmpty() && !startingContentWarning.startsWith(contentWarning.toString()) - val mediaChanged = !media.value.isNullOrEmpty() + val mediaChanged = media.value.isNotEmpty() val pollChanged = poll.value != null return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged 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 de7141a15..324540d12 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 @@ -23,7 +23,7 @@ import android.util.Log import android.webkit.MimeTypeMap import androidx.core.content.FileProvider import androidx.core.net.toUri -import at.connyduck.calladapter.networkresult.getOrThrow +import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia @@ -32,6 +32,7 @@ import com.keylesspalace.tusky.network.ProgressRequestBody 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.getServerErrorMessage import com.keylesspalace.tusky.util.randomAlphanumericString import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -73,6 +74,7 @@ class AudioSizeException : Exception() class VideoSizeException : Exception() class MediaTypeException : Exception() class CouldNotOpenFileException : Exception() +class UploadServerError(val errorMessage: String) : Exception() class MediaUploader @Inject constructor( private val context: Context, @@ -223,8 +225,16 @@ class MediaUploader @Inject constructor( null } - val result = mediaUploadApi.uploadMedia(body, description).getOrThrow() - send(UploadEvent.FinishedEvent(result.id)) + mediaUploadApi.uploadMedia(body, description).fold({ result -> + send(UploadEvent.FinishedEvent(result.id)) + }, { throwable -> + val errorMessage = throwable.getServerErrorMessage() + if (errorMessage == null) { + throw throwable + } else { + throw UploadServerError(errorMessage) + } + }) awaitClose() } } @@ -241,7 +251,7 @@ class MediaUploader @Inject constructor( } private companion object { - private const val TAG = "MediaUploaderImpl" + private const val TAG = "MediaUploader" private const val STATUS_VIDEO_SIZE_LIMIT = 41943040 // 40MiB private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ThrowableExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/ThrowableExtensions.kt new file mode 100644 index 000000000..26f962554 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ThrowableExtensions.kt @@ -0,0 +1,26 @@ +package com.keylesspalace.tusky.util + +import org.json.JSONException +import org.json.JSONObject +import retrofit2.HttpException + +/** + * checks if this throwable indicates an error causes by a 4xx/5xx server response and + * tries to retrieve the error message the server sent + * @return the error message, or null if this is no server error or it had no error message + */ +fun Throwable.getServerErrorMessage(): String? { + if (this is HttpException) { + val errorResponse = response()?.errorBody()?.string() + return if (!errorResponse.isNullOrBlank()) { + try { + JSONObject(errorResponse).getString("error") + } catch (e: JSONException) { + null + } + } else { + null + } + } + return null +} 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 72835acc4..bc7f435df 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -32,6 +32,7 @@ import com.keylesspalace.tusky.util.Error import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.getServerErrorMessage import com.keylesspalace.tusky.util.randomAlphanumericString import kotlinx.coroutines.launch import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -39,9 +40,6 @@ import okhttp3.MultipartBody import okhttp3.RequestBody import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.toRequestBody -import org.json.JSONException -import org.json.JSONObject -import retrofit2.HttpException import java.io.File import javax.inject.Inject @@ -156,21 +154,7 @@ class EditProfileViewModel @Inject constructor( 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 { - saveData.postValue(Error()) - } + saveData.postValue(Error(errorMessage = throwable.getServerErrorMessage())) } ) } diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml index 3219578b3..958de1b8d 100644 --- a/app/src/main/res/layout/activity_compose.xml +++ b/app/src/main/res/layout/activity_compose.xml @@ -239,6 +239,7 @@ app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" /> Date: Thu, 30 Jun 2022 21:25:44 +0200 Subject: [PATCH 67/82] add test for InstanceSwitchAuthInterceptor and convert it to Kotlin (#2596) * add test for InstanceSwitchAuthInterceptor * improve InstanceSwitchAuthInterceptorTest * Rename .java to .kt * convert InstanceSwitchAuthInterceptor to Kotlin * fix ktlint issues * improve InstanceSwitchAuthInterceptorTest * improve InstanceSwitchAuthInterceptorTest --- app/build.gradle | 2 + .../InstanceSwitchAuthInterceptor.java | 88 ----------- .../network/InstanceSwitchAuthInterceptor.kt | 82 ++++++++++ .../InstanceSwitchAuthInterceptorTest.kt | 143 ++++++++++++++++++ 4 files changed, 227 insertions(+), 88 deletions(-) delete mode 100644 app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java create mode 100644 app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.kt create mode 100644 app/src/test/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptorTest.kt diff --git a/app/build.gradle b/app/build.gradle index b1f08360e..406832c17 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -189,6 +189,8 @@ dependencies { testImplementation "org.mockito:mockito-inline:4.4.0" testImplementation "org.mockito.kotlin:mockito-kotlin:4.0.0" + testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:3.4.0" androidTestImplementation "androidx.room:room-testing:$roomVersion" androidTestImplementation "androidx.test.ext:junit:1.1.3" diff --git a/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java b/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java deleted file mode 100644 index a3e1a815f..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.java +++ /dev/null @@ -1,88 +0,0 @@ -/* Copyright 2018 charlag - * - * 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.network; - -import android.util.Log; -import androidx.annotation.NonNull; - -import com.keylesspalace.tusky.db.AccountEntity; -import com.keylesspalace.tusky.db.AccountManager; - -import java.io.IOException; - -import okhttp3.*; - -/** - * Created by charlag on 31/10/17. - */ - -public final class InstanceSwitchAuthInterceptor implements Interceptor { - private final AccountManager accountManager; - - public InstanceSwitchAuthInterceptor(AccountManager accountManager) { - this.accountManager = accountManager; - } - - @NonNull - @Override - public Response intercept(@NonNull Chain chain) throws IOException { - - Request originalRequest = chain.request(); - - // only switch domains if the request comes from retrofit - if (originalRequest.url().host().equals(MastodonApi.PLACEHOLDER_DOMAIN)) { - AccountEntity currentAccount = accountManager.getActiveAccount(); - - Request.Builder builder = originalRequest.newBuilder(); - - String instanceHeader = originalRequest.header(MastodonApi.DOMAIN_HEADER); - if (instanceHeader != null) { - // use domain explicitly specified in custom header - builder.url(swapHost(originalRequest.url(), instanceHeader)); - builder.removeHeader(MastodonApi.DOMAIN_HEADER); - } else if (currentAccount != null) { - String accessToken = currentAccount.getAccessToken(); - if (!accessToken.isEmpty()) { - //use domain of current account - builder.url(swapHost(originalRequest.url(), currentAccount.getDomain())) - .header("Authorization", - String.format("Bearer %s", currentAccount.getAccessToken())); - } - } - Request newRequest = builder.build(); - - if (MastodonApi.PLACEHOLDER_DOMAIN.equals(newRequest.url().host())) { - Log.w("ISAInterceptor", "no user logged in or no domain header specified - can't make request to " + newRequest.url()); - return new Response.Builder() - .code(400) - .message("Bad Request") - .protocol(Protocol.HTTP_2) - .body(ResponseBody.create("", MediaType.parse("text/plain"))) - .request(chain.request()) - .build(); - } - return chain.proceed(newRequest); - - } else { - return chain.proceed(originalRequest); - } - } - - @NonNull - private static HttpUrl swapHost(@NonNull HttpUrl url, @NonNull String host) { - return url.newBuilder().host(host).build(); - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.kt b/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.kt new file mode 100644 index 000000000..3ca7a8116 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.kt @@ -0,0 +1,82 @@ +/* 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.network + +import android.util.Log +import com.keylesspalace.tusky.db.AccountManager +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody +import java.io.IOException + +class InstanceSwitchAuthInterceptor(private val accountManager: AccountManager) : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest: Request = chain.request() + + // only switch domains if the request comes from retrofit + return if (originalRequest.url.host == MastodonApi.PLACEHOLDER_DOMAIN) { + + val builder: Request.Builder = originalRequest.newBuilder() + val instanceHeader = originalRequest.header(MastodonApi.DOMAIN_HEADER) + + if (instanceHeader != null) { + // use domain explicitly specified in custom header + builder.url(swapHost(originalRequest.url, instanceHeader)) + builder.removeHeader(MastodonApi.DOMAIN_HEADER) + } else { + val currentAccount = accountManager.activeAccount + + if (currentAccount != null) { + val accessToken = currentAccount.accessToken + if (accessToken.isNotEmpty()) { + // use domain of current account + builder.url(swapHost(originalRequest.url, currentAccount.domain)) + .header("Authorization", "Bearer %s".format(accessToken)) + } + } + } + + val newRequest: Request = builder.build() + + if (MastodonApi.PLACEHOLDER_DOMAIN == newRequest.url.host) { + Log.w("ISAInterceptor", "no user logged in or no domain header specified - can't make request to " + newRequest.url) + return Response.Builder() + .code(400) + .message("Bad Request") + .protocol(Protocol.HTTP_2) + .body("".toResponseBody("text/plain".toMediaType())) + .request(chain.request()) + .build() + } + + chain.proceed(newRequest) + } else { + chain.proceed(originalRequest) + } + } + + companion object { + private fun swapHost(url: HttpUrl, host: String): HttpUrl { + return url.newBuilder().host(host).build() + } + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptorTest.kt b/app/src/test/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptorTest.kt new file mode 100644 index 000000000..aa070489d --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptorTest.kt @@ -0,0 +1,143 @@ +package com.keylesspalace.tusky.network + +import com.keylesspalace.tusky.db.AccountEntity +import com.keylesspalace.tusky.db.AccountManager +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock + +class InstanceSwitchAuthInterceptorTest { + + private val mockWebServer = MockWebServer() + + @Before + fun setup() { + mockWebServer.start() + } + + @After + fun teardown() { + mockWebServer.shutdown() + } + + @Test + fun `should make regular request when requested`() { + + mockWebServer.enqueue(MockResponse()) + + val accountManager: AccountManager = mock { + on { activeAccount } doAnswer { null } + } + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) + .build() + + val request = Request.Builder() + .get() + .url(mockWebServer.url("/test")) + .build() + + val response = okHttpClient.newCall(request).execute() + + assertEquals(200, response.code) + } + + @Test + fun `should make request to instance requested in special header`() { + mockWebServer.enqueue(MockResponse()) + + val accountManager: AccountManager = mock { + on { activeAccount } doAnswer { + AccountEntity( + id = 1, + domain = "test.domain", + accessToken = "fakeToken", + clientId = "fakeId", + clientSecret = "fakeSecret", + isActive = true + ) + } + } + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) + .build() + + val request = Request.Builder() + .get() + .url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + ":" + mockWebServer.port + "/test") + .header(MastodonApi.DOMAIN_HEADER, mockWebServer.hostName) + .build() + + val response = okHttpClient.newCall(request).execute() + + assertEquals(200, response.code) + + assertNull(mockWebServer.takeRequest().getHeader("Authorization")) + } + + @Test + fun `should make request to current instance when requested and user is logged in`() { + mockWebServer.enqueue(MockResponse()) + + val accountManager: AccountManager = mock { + on { activeAccount } doAnswer { + AccountEntity( + id = 1, + domain = mockWebServer.hostName, + accessToken = "fakeToken", + clientId = "fakeId", + clientSecret = "fakeSecret", + isActive = true + ) + } + } + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) + .build() + + val request = Request.Builder() + .get() + .url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + ":" + mockWebServer.port + "/test") + .build() + + val response = okHttpClient.newCall(request).execute() + + assertEquals(200, response.code) + + assertEquals("Bearer fakeToken", mockWebServer.takeRequest().getHeader("Authorization")) + } + + @Test + fun `should fail to make request when request to current instance is requested but no user is logged in`() { + mockWebServer.enqueue(MockResponse()) + + val accountManager: AccountManager = mock { + on { activeAccount } doAnswer { null } + } + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) + .build() + + val request = Request.Builder() + .get() + .url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + "/test") + .build() + + val response = okHttpClient.newCall(request).execute() + + assertEquals(400, response.code) + assertEquals(0, mockWebServer.requestCount) + } +} From 8067ef863e2fd0abd1940d563f8e66e6feb79147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Ho=C5=99=C3=A1nek?= Date: Sat, 2 Jul 2022 16:26:52 +0000 Subject: [PATCH 68/82] Translated using Weblate (Czech) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 86.0% (421 of 489 strings) Co-authored-by: Šimon Hořánek Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/cs/ Translation: Tusky/Tusky --- app/src/main/res/values-cs/strings.xml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index bd74ce631..aced0945d 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -198,7 +198,7 @@ Výchozí soukromí příspěvků Vždy označovat média jako citlivá Publikování (synchronizováno se serverem) - Nepodařilo se synchronizovsat nastavení + Nepodařilo se synchronizovat nastavení Veřejné Neuvedené Pouze pro sledující @@ -483,4 +483,12 @@ Zobrazit dialogové okno s potvrzením při boostování %s právě vydal Oznámení + Přihlášení + %s se zaregistroval + Přihlaste se znovu pro oznámení + Nepodařilo se načíst stránku přihlášení. + Tento příspěvek se nepodařilo poslat! + Nepodařilo se načíst detaily účtu + Nepodařilo se načíst informace o odpovědi + Obrázek se nepodařilo upravit. \ No newline at end of file From b3eeb9bcfa715ed3f1587a1497924a4799ae6aaa Mon Sep 17 00:00:00 2001 From: Eric Date: Sat, 2 Jul 2022 16:26:53 +0000 Subject: [PATCH 69/82] Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (489 of 489 strings) Co-authored-by: Eric Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/zh_Hans/ Translation: Tusky/Tusky --- app/src/main/res/values-zh-rCN/strings.xml | 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 3ca2c1d55..99a329c41 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -546,4 +546,5 @@ 1+ 编辑图片 无法编辑图片。 + 加载账户详情失败 \ No newline at end of file From 4fccd1a93a5208a5054cf6d83b8e50f964a430a3 Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Sat, 2 Jul 2022 16:26:53 +0000 Subject: [PATCH 70/82] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (489 of 489 strings) Co-authored-by: Vegard Skjefstad Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/nb_NO/ Translation: Tusky/Tusky --- app/src/main/res/values-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 1ac18f3a5..2fbaa7557 100644 --- a/app/src/main/res/values-no-rNB/strings.xml +++ b/app/src/main/res/values-no-rNB/strings.xml @@ -538,4 +538,5 @@ 1+ Rediger bilde Bildet kunne ikke redigeres. + Lasting av kontodetaljer feilet \ No newline at end of file From ad4163d319c2b38e34246be038ebcf7c0951ca71 Mon Sep 17 00:00:00 2001 From: Ihor Hordiichuk Date: Sat, 2 Jul 2022 16:26:53 +0000 Subject: [PATCH 71/82] Translated using Weblate (Ukrainian) Currently translated at 100.0% (489 of 489 strings) Co-authored-by: Ihor Hordiichuk Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/uk/ Translation: Tusky/Tusky --- app/src/main/res/values-uk/strings.xml | 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 489f872ab..1a9709441 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -560,4 +560,5 @@ Редагувати зображення 1+ Неможливо редагувати зображення. + Не вдалося завантажити подробиці облікового запису \ No newline at end of file From ae5364612ee975eb8d49fdb5f8da171b1e3faa14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=E1=BB=93=20Nh=E1=BA=A5t=20Duy?= Date: Sat, 2 Jul 2022 16:26:53 +0000 Subject: [PATCH 72/82] Translated using Weblate (Vietnamese) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (489 of 489 strings) Co-authored-by: Hồ Nhất Duy Translate-URL: https://weblate.tusky.app/projects/tusky/tusky/vi/ Translation: Tusky/Tusky --- app/src/main/res/values-vi/strings.xml | 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 024741bff..7eb9ba336 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -527,4 +527,5 @@ 1+ Sửa ảnh Hình ảnh này không thể sửa. + Không thể tải thông tin tài khoản \ No newline at end of file From a8a97b68347543725d3c98dacf2bc4855ef1642b Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 5 Jul 2022 18:18:12 +0200 Subject: [PATCH 73/82] fix relogin bug (#2609) * fix relogin bug * fix ktlint --- .../keylesspalace/tusky/db/AccountManager.kt | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt index 04f8e6f56..9c5e118b3 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt @@ -72,14 +72,18 @@ class AccountManager @Inject constructor(db: AppDatabase) { accountDao.insertOrReplace(it) } // check if this is a relogin with an existing account, if yes update it, otherwise create a new one - val newAccountEntity = accounts.find { account -> + val existingAccountIndex = accounts.indexOfFirst { account -> domain == account.domain && newAccount.id == account.accountId - }?.copy( - accessToken = accessToken, - clientId = clientId, - clientSecret = clientSecret, - oauthScopes = oauthScopes - ) ?: run { + } + val newAccountEntity = if (existingAccountIndex != -1) { + accounts[existingAccountIndex].copy( + accessToken = accessToken, + clientId = clientId, + clientSecret = clientSecret, + oauthScopes = oauthScopes, + isActive = true + ).also { accounts[existingAccountIndex] = it } + } else { val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0 val newAccountId = maxAccountId + 1 AccountEntity( From 3a4a7d8701a14f57f0da367648ec53a8d6aa34f4 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Tue, 5 Jul 2022 18:30:57 +0200 Subject: [PATCH 74/82] Release 93 --- app/build.gradle | 4 ++-- fastlane/metadata/android/en-US/changelogs/{93.txt => 94.txt} | 0 fastlane/metadata/android/fr/changelogs/{93.txt => 94.txt} | 0 fastlane/metadata/android/gl/changelogs/{93.txt => 94.txt} | 0 fastlane/metadata/android/hu/changelogs/{93.txt => 94.txt} | 0 fastlane/metadata/android/uk/changelogs/{93.txt => 94.txt} | 0 fastlane/metadata/android/vi/changelogs/{93.txt => 94.txt} | 0 7 files changed, 2 insertions(+), 2 deletions(-) rename fastlane/metadata/android/en-US/changelogs/{93.txt => 94.txt} (100%) rename fastlane/metadata/android/fr/changelogs/{93.txt => 94.txt} (100%) rename fastlane/metadata/android/gl/changelogs/{93.txt => 94.txt} (100%) rename fastlane/metadata/android/hu/changelogs/{93.txt => 94.txt} (100%) rename fastlane/metadata/android/uk/changelogs/{93.txt => 94.txt} (100%) rename fastlane/metadata/android/vi/changelogs/{93.txt => 94.txt} (100%) diff --git a/app/build.gradle b/app/build.gradle index 406832c17..2d04fb0ef 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,8 +24,8 @@ android { applicationId APP_ID minSdkVersion 21 targetSdkVersion 31 - versionCode 92 - versionName "19.0 beta 1" + versionCode 93 + versionName "19.0 beta 2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true diff --git a/fastlane/metadata/android/en-US/changelogs/93.txt b/fastlane/metadata/android/en-US/changelogs/94.txt similarity index 100% rename from fastlane/metadata/android/en-US/changelogs/93.txt rename to fastlane/metadata/android/en-US/changelogs/94.txt diff --git a/fastlane/metadata/android/fr/changelogs/93.txt b/fastlane/metadata/android/fr/changelogs/94.txt similarity index 100% rename from fastlane/metadata/android/fr/changelogs/93.txt rename to fastlane/metadata/android/fr/changelogs/94.txt diff --git a/fastlane/metadata/android/gl/changelogs/93.txt b/fastlane/metadata/android/gl/changelogs/94.txt similarity index 100% rename from fastlane/metadata/android/gl/changelogs/93.txt rename to fastlane/metadata/android/gl/changelogs/94.txt diff --git a/fastlane/metadata/android/hu/changelogs/93.txt b/fastlane/metadata/android/hu/changelogs/94.txt similarity index 100% rename from fastlane/metadata/android/hu/changelogs/93.txt rename to fastlane/metadata/android/hu/changelogs/94.txt diff --git a/fastlane/metadata/android/uk/changelogs/93.txt b/fastlane/metadata/android/uk/changelogs/94.txt similarity index 100% rename from fastlane/metadata/android/uk/changelogs/93.txt rename to fastlane/metadata/android/uk/changelogs/94.txt diff --git a/fastlane/metadata/android/vi/changelogs/93.txt b/fastlane/metadata/android/vi/changelogs/94.txt similarity index 100% rename from fastlane/metadata/android/vi/changelogs/93.txt rename to fastlane/metadata/android/vi/changelogs/94.txt From 2811259b99223c2de3601ff7cfce75f673fc4aac Mon Sep 17 00:00:00 2001 From: Connyduck Date: Sun, 10 Jul 2022 18:26:56 +0000 Subject: [PATCH 75/82] Translated using Weblate (German) Currently translated at 94.4% (17 of 18 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/de/ --- fastlane/metadata/android/de/changelogs/94.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 fastlane/metadata/android/de/changelogs/94.txt diff --git a/fastlane/metadata/android/de/changelogs/94.txt b/fastlane/metadata/android/de/changelogs/94.txt new file mode 100644 index 000000000..711131ca3 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Push-Benachrichtigungen via Unified Push. Um Unified Push zu verwenden musst du dich neu einloggen. +- Die Anzahl an Antworten unter einem Beitrag wird jetzt in der Timeline angezeigt. +- Bilder können jetzt vor dem Veröffentlichen zugeschnitten werden. +- Das Erstellungsdatum eines Profils wird jetzt angezeigt. +- Beim Betrachten einer Liste ist jetzt der Listenname ersichtlich. +- Fehlerbehebungen +- verbesserte Übersetzungen From 68d4c71efad30f002191f2c38ec5c3269a23f319 Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Sun, 10 Jul 2022 18:26:56 +0000 Subject: [PATCH 76/82] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (18 of 18 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/nb_NO/ --- fastlane/metadata/android/nb-NO/changelogs/91.txt | 6 ++++++ fastlane/metadata/android/nb-NO/changelogs/94.txt | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 fastlane/metadata/android/nb-NO/changelogs/91.txt create mode 100644 fastlane/metadata/android/nb-NO/changelogs/94.txt diff --git a/fastlane/metadata/android/nb-NO/changelogs/91.txt b/fastlane/metadata/android/nb-NO/changelogs/91.txt new file mode 100644 index 000000000..fe3c8e0d2 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Støtte for Mastodon 3.5-varslingstyper +- Bot-symbolet ser nå bedre ut og endrer seg basert på valgt tema +- Det er nå mulig å markere tekst i skjermbildet som viser innleggsdetaljer +- Fikset flere feil, inkludert en som hindret innlogging på Android 6 og eldre versjoner diff --git a/fastlane/metadata/android/nb-NO/changelogs/94.txt b/fastlane/metadata/android/nb-NO/changelogs/94.txt new file mode 100644 index 000000000..954a80185 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Støtte for Unified Push. For å aktivisere dette må du logg inne på kontoene dine på nytt. +- Antall tilbakemeldinger på et innlegg vises nå i tidslinjene. +- Bilder kan nå beskjæres når innlegget opprettes. +- Dato nå en profil ble opprettes vises. +- Visning av liste viser nå navnet på listen i verktøylinjen. +- En mengde feilfikser. +- Oppdaterte oversettelser. From 526f170d516c6c08357e276e08121d0597a86011 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Mon, 11 Jul 2022 18:17:58 +0200 Subject: [PATCH 77/82] don't crash on invalid json response for push subscription (#2613) --- .../notifications/PushNotificationHelper.kt | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt index 68a443027..0d804dd97 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/PushNotificationHelper.kt @@ -157,7 +157,14 @@ private fun buildSubscriptionData(context: Context, account: AccountEntity): Map } // Called by UnifiedPush callback -suspend fun registerUnifiedPushEndpoint(context: Context, api: MastodonApi, accountManager: AccountManager, account: AccountEntity, endpoint: String) { +suspend fun registerUnifiedPushEndpoint( + context: Context, + api: MastodonApi, + accountManager: AccountManager, + account: AccountEntity, + endpoint: String +) = withContext(Dispatchers.IO) { + // Generate a prime256v1 key pair for WebPush // Decryption is unimplemented for now, since Mastodon uses an old WebPush // standard which does not send needed information for decryption in the payload @@ -166,27 +173,22 @@ suspend fun registerUnifiedPushEndpoint(context: Context, api: MastodonApi, acco val keyPair = CryptoUtil.generateECKeyPair(CryptoUtil.CURVE_PRIME256_V1) val auth = CryptoUtil.secureRandomBytesEncoded(16) - withContext(Dispatchers.IO) { - api.subscribePushNotifications( - "Bearer ${account.accessToken}", account.domain, - endpoint, keyPair.pubkey, auth, - buildSubscriptionData(context, account) - ).onFailure { - Log.d(TAG, "Error setting push endpoint for account ${account.id}") - Log.d(TAG, Log.getStackTraceString(it)) - Log.d(TAG, (it as HttpException).response().toString()) + api.subscribePushNotifications( + "Bearer ${account.accessToken}", account.domain, + endpoint, keyPair.pubkey, auth, + buildSubscriptionData(context, account) + ).onFailure { throwable -> + Log.w(TAG, "Error setting push endpoint for account ${account.id}", throwable) + disableUnifiedPushNotificationsForAccount(context, account) + }.onSuccess { + Log.d(TAG, "UnifiedPush registration succeeded for account ${account.id}") - disableUnifiedPushNotificationsForAccount(context, account) - }.onSuccess { - Log.d(TAG, "UnifiedPush registration succeeded for account ${account.id}") - - account.pushPubKey = keyPair.pubkey - account.pushPrivKey = keyPair.privKey - account.pushAuth = auth - account.pushServerKey = it.serverKey - account.unifiedPushUrl = endpoint - accountManager.saveAccount(account) - } + account.pushPubKey = keyPair.pubkey + account.pushPrivKey = keyPair.privKey + account.pushAuth = auth + account.pushServerKey = it.serverKey + account.unifiedPushUrl = endpoint + accountManager.saveAccount(account) } } From 90a5d842d4ae010d15485fe9321544bc5a7bedbb Mon Sep 17 00:00:00 2001 From: Connyduck Date: Sun, 10 Jul 2022 18:26:56 +0000 Subject: [PATCH 78/82] Translated using Weblate (German) Currently translated at 94.4% (17 of 18 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/de/ --- fastlane/metadata/android/de/changelogs/94.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 fastlane/metadata/android/de/changelogs/94.txt diff --git a/fastlane/metadata/android/de/changelogs/94.txt b/fastlane/metadata/android/de/changelogs/94.txt new file mode 100644 index 000000000..711131ca3 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Push-Benachrichtigungen via Unified Push. Um Unified Push zu verwenden musst du dich neu einloggen. +- Die Anzahl an Antworten unter einem Beitrag wird jetzt in der Timeline angezeigt. +- Bilder können jetzt vor dem Veröffentlichen zugeschnitten werden. +- Das Erstellungsdatum eines Profils wird jetzt angezeigt. +- Beim Betrachten einer Liste ist jetzt der Listenname ersichtlich. +- Fehlerbehebungen +- verbesserte Übersetzungen From 57cfe512788adcee5fdac22b32e8a2d1f91d5cca Mon Sep 17 00:00:00 2001 From: Vegard Skjefstad Date: Sun, 10 Jul 2022 18:26:56 +0000 Subject: [PATCH 79/82] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegian?= =?UTF-8?q?=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (18 of 18 strings) Translation: Tusky/Tusky description Translate-URL: https://weblate.tusky.app/projects/tusky/tusky-app/nb_NO/ --- fastlane/metadata/android/nb-NO/changelogs/91.txt | 6 ++++++ fastlane/metadata/android/nb-NO/changelogs/94.txt | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 fastlane/metadata/android/nb-NO/changelogs/91.txt create mode 100644 fastlane/metadata/android/nb-NO/changelogs/94.txt diff --git a/fastlane/metadata/android/nb-NO/changelogs/91.txt b/fastlane/metadata/android/nb-NO/changelogs/91.txt new file mode 100644 index 000000000..fe3c8e0d2 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Støtte for Mastodon 3.5-varslingstyper +- Bot-symbolet ser nå bedre ut og endrer seg basert på valgt tema +- Det er nå mulig å markere tekst i skjermbildet som viser innleggsdetaljer +- Fikset flere feil, inkludert en som hindret innlogging på Android 6 og eldre versjoner diff --git a/fastlane/metadata/android/nb-NO/changelogs/94.txt b/fastlane/metadata/android/nb-NO/changelogs/94.txt new file mode 100644 index 000000000..954a80185 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Støtte for Unified Push. For å aktivisere dette må du logg inne på kontoene dine på nytt. +- Antall tilbakemeldinger på et innlegg vises nå i tidslinjene. +- Bilder kan nå beskjæres når innlegget opprettes. +- Dato nå en profil ble opprettes vises. +- Visning av liste viser nå navnet på listen i verktøylinjen. +- En mengde feilfikser. +- Oppdaterte oversettelser. From 5ed0cc24a5df852a6748db9cc1d8942317868ec1 Mon Sep 17 00:00:00 2001 From: codl Date: Mon, 11 Jul 2022 16:16:19 +0000 Subject: [PATCH 80/82] Translated using Weblate (French) Currently translated at 99.7% (488 of 489 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 | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 5025ae1d2..d712bc215 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -249,11 +249,11 @@ Vidéo Demande d’abonnement effectuée - en %da - en %dj - en %d h - en %dm - en %ds + dans %da + dans %dj + dans %dh + dans %dm + dans %ds %d a %dj %d h From 3a7d89189d091bbaa15b8a9cdf863a36ca01f840 Mon Sep 17 00:00:00 2001 From: "Gera, Zoltan" Date: Mon, 11 Jul 2022 16:16:19 +0000 Subject: [PATCH 81/82] Translated using Weblate (Hungarian) Currently translated at 100.0% (489 of 489 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 | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index d133c3ac8..a097e81e6 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -546,4 +546,5 @@ Újra bejelentkeztél a fiókodba, hogy feliratkoztasd a Tusky-t a leküldési értesítések használatára. Ugyanakkor vannak még fiókjaid, melyek még nem lettek így migrálva. Válts át rájuk és jelentkezz be újra mindegyikben, hogy ezekben is engedélyezd a UnifiedPush értesítések támogatását. Kép szerkesztése A kép nem szerkeszthető. + Nem sikerült betölteni a fiókadatokat \ No newline at end of file From 3ae18fd9231ba9d8f36df4b8187488b9e2cb2f01 Mon Sep 17 00:00:00 2001 From: Conny Duck Date: Mon, 11 Jul 2022 18:41:21 +0200 Subject: [PATCH 82/82] Release 94 --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 2d04fb0ef..70649f429 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -24,8 +24,8 @@ android { applicationId APP_ID minSdkVersion 21 targetSdkVersion 31 - versionCode 93 - versionName "19.0 beta 2" + versionCode 94 + versionName "19.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables.useSupportLibrary = true