From 1b6a0908f63be393f47fc65505e90ac7cf239462 Mon Sep 17 00:00:00 2001 From: Konrad Pozniak Date: Tue, 26 Jul 2022 20:24:50 +0200 Subject: [PATCH] Handle even more instance defaults (#2612) * handle media size instance limits * remove unused attributes from Instance entity * support max_media_attachments * support pleroma field limits, remove max_bio_chars support * improve field input margin * fix tests * MAX_ACCOUNT_FIELDS -> DEFAULT_MAX_ACCOUNT_FIELDS * improve "add field" button behavior * fix copy paste mistake in AccountFieldEditAdapter * refactor sendStatus to be a suspending function --- .../40.json | 929 ++++++++++++++++++ .../tusky/EditProfileActivity.kt | 25 +- .../tusky/adapter/AccountFieldEditAdapter.kt | 26 +- .../components/compose/ComposeActivity.kt | 111 ++- .../components/compose/ComposeViewModel.kt | 98 +- .../tusky/components/compose/MediaUploader.kt | 25 +- .../components/instanceinfo/InstanceInfo.kt | 9 +- .../instanceinfo/InstanceInfoRepository.kt | 25 +- .../keylesspalace/tusky/db/AppDatabase.java | 15 +- .../keylesspalace/tusky/db/InstanceEntity.kt | 18 +- .../com/keylesspalace/tusky/di/AppModule.kt | 2 +- .../keylesspalace/tusky/entity/Instance.kt | 33 +- .../keylesspalace/tusky/util/LiveDataUtil.kt | 98 -- .../tusky/viewmodel/EditProfileViewModel.kt | 31 +- app/src/main/res/layout/item_edit_field.xml | 38 +- .../tusky/ComposeActivityTest.kt | 44 +- 16 files changed, 1219 insertions(+), 308 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/40.json delete mode 100644 app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/40.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/40.json new file mode 100644 index 00000000..54d5a2bf --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/40.json @@ -0,0 +1,929 @@ +{ + "formatVersion": 1, + "database": { + "version": 40, + "identityHash": "0423fb3f7d09db5f12023f2f4e7297b5", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0423fb3f7d09db5f12023f2f4e7297b5')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt index b749fe93..369f6926 100644 --- a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -28,6 +28,7 @@ import android.widget.ImageView import androidx.activity.viewModels import androidx.core.view.isVisible import androidx.lifecycle.LiveData +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.bumptech.glide.Glide import com.bumptech.glide.load.engine.DiskCacheStrategy @@ -37,6 +38,7 @@ import com.canhub.cropper.CropImageContract import com.canhub.cropper.options import com.google.android.material.snackbar.Snackbar import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding import com.keylesspalace.tusky.di.Injectable import com.keylesspalace.tusky.di.ViewModelFactory @@ -50,6 +52,7 @@ import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp +import kotlinx.coroutines.launch import javax.inject.Inject class EditProfileActivity : BaseActivity(), Injectable { @@ -58,8 +61,6 @@ class EditProfileActivity : BaseActivity(), Injectable { const val AVATAR_SIZE = 400 const val HEADER_WIDTH = 1500 const val HEADER_HEIGHT = 500 - - private const val MAX_ACCOUNT_FIELDS = 4 } @Inject @@ -71,6 +72,8 @@ class EditProfileActivity : BaseActivity(), Injectable { private val accountFieldEditAdapter = AccountFieldEditAdapter() + private var maxAccountFields = InstanceInfoRepository.DEFAULT_MAX_ACCOUNT_FIELDS + private enum class PickType { AVATAR, HEADER @@ -112,7 +115,7 @@ class EditProfileActivity : BaseActivity(), Injectable { binding.addFieldButton.setOnClickListener { accountFieldEditAdapter.addField() - if (accountFieldEditAdapter.itemCount >= MAX_ACCOUNT_FIELDS) { + if (accountFieldEditAdapter.itemCount >= maxAccountFields) { it.isVisible = false } @@ -134,7 +137,8 @@ class EditProfileActivity : BaseActivity(), Injectable { binding.lockedCheckBox.isChecked = me.locked accountFieldEditAdapter.setFields(me.source?.fields ?: emptyList()) - binding.addFieldButton.isEnabled = me.source?.fields?.size ?: 0 < MAX_ACCOUNT_FIELDS + binding.addFieldButton.isVisible = + (me.source?.fields?.size ?: 0) < maxAccountFields if (viewModel.avatarData.value == null) { Glide.with(this) @@ -165,13 +169,12 @@ class EditProfileActivity : BaseActivity(), Injectable { } } - viewModel.obtainInstance() - viewModel.instanceData.observe(this) { result -> - if (result is Success) { - val instance = result.data - if (instance?.maxBioChars != null && instance.maxBioChars > 0) { - binding.noteEditTextLayout.counterMaxLength = instance.maxBioChars - } + lifecycleScope.launch { + viewModel.instanceData.collect { instanceInfo -> + maxAccountFields = instanceInfo.maxFields + accountFieldEditAdapter.setFieldLimits(instanceInfo.maxFieldNameLength, instanceInfo.maxFieldValueLength) + binding.addFieldButton.isVisible = + accountFieldEditAdapter.itemCount < maxAccountFields } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt index 7ba5537b..30cf6309 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt @@ -27,6 +27,8 @@ import com.keylesspalace.tusky.util.BindingHolder class AccountFieldEditAdapter : RecyclerView.Adapter>() { private val fieldData = mutableListOf() + private var maxNameLength: Int? = null + private var maxValueLength: Int? = null fun setFields(fields: List) { fieldData.clear() @@ -41,6 +43,12 @@ class AccountFieldEditAdapter : RecyclerView.Adapter { return fieldData.map { StringField(it.first, it.second) @@ -60,10 +68,20 @@ class AccountFieldEditAdapter : RecyclerView.Adapter, position: Int) { - holder.binding.accountFieldName.setText(fieldData[position].first) - holder.binding.accountFieldValue.setText(fieldData[position].second) + holder.binding.accountFieldNameText.setText(fieldData[position].first) + holder.binding.accountFieldValueText.setText(fieldData[position].second) - holder.binding.accountFieldName.addTextChangedListener(object : TextWatcher { + holder.binding.accountFieldNameTextLayout.isCounterEnabled = maxNameLength != null + maxNameLength?.let { + holder.binding.accountFieldNameTextLayout.counterMaxLength = it + } + + holder.binding.accountFieldValueTextLayout.isCounterEnabled = maxValueLength != null + maxValueLength?.let { + holder.binding.accountFieldValueTextLayout.counterMaxLength = it + } + + holder.binding.accountFieldNameText.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(newText: Editable) { fieldData[holder.bindingAdapterPosition].first = newText.toString() } @@ -73,7 +91,7 @@ class AccountFieldEditAdapter : RecyclerView.Adapter if (success) { @@ -147,7 +145,7 @@ class ComposeActivity : } } private val pickMediaFile = registerForActivityResult(PickMediaFiles()) { uris -> - if (mediaCount + uris.size > maxUploadMediaNumber) { + if (viewModel.media.value.size + uris.size > maxUploadMediaNumber) { Toast.makeText(this, resources.getQuantityString(R.plurals.error_upload_max_media_reached, maxUploadMediaNumber, maxUploadMediaNumber), Toast.LENGTH_SHORT).show() } else { uris.forEach { uri -> @@ -224,8 +222,8 @@ class ComposeActivity : binding.composeMediaPreviewBar.adapter = mediaAdapter binding.composeMediaPreviewBar.itemAnimator = null - subscribeToUpdates(mediaAdapter) setupButtons() + subscribeToUpdates(mediaAdapter) photoUploadUri = savedInstanceState?.getParcelable(PHOTO_UPLOAD_URI_KEY) @@ -363,36 +361,48 @@ class ComposeActivity : } private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter) { - withLifecycleContext { - viewModel.instanceInfo.observe { instanceData -> + lifecycleScope.launch { + viewModel.instanceInfo.collect { instanceData -> maximumTootCharacters = instanceData.maxChars charactersReservedPerUrl = instanceData.charactersReservedPerUrl + maxUploadMediaNumber = instanceData.maxMediaAttachments updateVisibleCharactersLeft() } - viewModel.emoji.observe { emoji -> setEmojiList(emoji) } - combineLiveData(viewModel.markMediaAsSensitive, viewModel.showContentWarning) { markSensitive, showContentWarning -> + } + + lifecycleScope.launch { + viewModel.emoji.collect(::setEmojiList) + } + + lifecycleScope.launch { + viewModel.showContentWarning.combine(viewModel.markMediaAsSensitive) { showContentWarning, markSensitive -> updateSensitiveMediaToggle(markSensitive, showContentWarning) showContentWarning(showContentWarning) - }.subscribe() - viewModel.statusVisibility.observe { visibility -> - setStatusVisibility(visibility) - } - lifecycleScope.launch { - viewModel.media.collect { media -> - mediaAdapter.submitList(media) - if (media.size != mediaCount) { - mediaCount = media.size - binding.composeMediaPreviewBar.visible(media.isNotEmpty()) - updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value != false, viewModel.showContentWarning.value != false) - } - } - } + }.collect() + } - viewModel.poll.observe { poll -> + lifecycleScope.launch { + viewModel.statusVisibility.collect(::setStatusVisibility) + } + + lifecycleScope.launch { + viewModel.media.collect { media -> + mediaAdapter.submitList(media) + + binding.composeMediaPreviewBar.visible(media.isNotEmpty()) + updateSensitiveMediaToggle(viewModel.markMediaAsSensitive.value, viewModel.showContentWarning.value) + } + } + + lifecycleScope.launch { + viewModel.poll.collect { poll -> binding.pollPreview.visible(poll != null) poll?.let(binding.pollPreview::setPoll) } - viewModel.scheduledAt.observe { scheduledAt -> + } + + lifecycleScope.launch { + viewModel.scheduledAt.collect { scheduledAt -> if (scheduledAt == null) { binding.composeScheduleView.resetSchedule() } else { @@ -400,22 +410,30 @@ class ComposeActivity : } updateScheduleButton() } - combineOptionalLiveData(viewModel.media.asLiveData(), viewModel.poll) { media, poll -> + } + + lifecycleScope.launch { + viewModel.media.combine(viewModel.poll) { media, poll -> val active = poll == null && - media!!.size != 4 && + media.size < maxUploadMediaNumber && (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE) enableButton(binding.composeAddMediaButton, active, active) - enablePollButton(media.isNullOrEmpty()) - }.subscribe() - viewModel.uploadError.observe { throwable -> - Log.w(TAG, "media upload failed", throwable) + enablePollButton(media.isEmpty()) + }.collect() + } + + lifecycleScope.launch { + viewModel.uploadError.collect { throwable -> if (throwable is UploadServerError) { displayTransientError(throwable.errorMessage) } else { displayTransientError(R.string.error_media_upload_sending) } } - viewModel.setupComplete.observe { + } + + lifecycleScope.launch { + viewModel.setupComplete.collect { // Focus may have changed during view model setup, ensure initial focus is on the edit field binding.composeEditField.requestFocus() } @@ -711,13 +729,17 @@ class ComposeActivity : addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED } - private fun openPollDialog() { + private fun openPollDialog() = lifecycleScope.launch { addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED - val instanceParams = viewModel.instanceInfo.value!! + val instanceParams = viewModel.instanceInfo.first() showAddPollDialog( - this, viewModel.poll.value, instanceParams.pollMaxOptions, - instanceParams.pollMaxLength, instanceParams.pollMinDuration, instanceParams.pollMaxDuration, - viewModel::updatePoll + context = this@ComposeActivity, + poll = viewModel.poll.value, + maxOptionCount = instanceParams.pollMaxOptions, + maxOptionLength = instanceParams.pollMaxLength, + minDuration = instanceParams.pollMinDuration, + maxDuration = instanceParams.pollMaxDuration, + onUpdatePoll = viewModel::updatePoll ) } @@ -768,7 +790,7 @@ class ComposeActivity : } } var length = binding.composeEditField.length() - offset - if (viewModel.showContentWarning.value!!) { + if (viewModel.showContentWarning.value) { length += binding.composeContentWarningField.length() } return length @@ -822,7 +844,7 @@ class ComposeActivity : enableButtons(false) val contentText = binding.composeEditField.text.toString() var spoilerText = "" - if (viewModel.showContentWarning.value!!) { + if (viewModel.showContentWarning.value) { spoilerText = binding.composeContentWarningField.text.toString() } val characterCount = calculateTextLength() @@ -837,9 +859,8 @@ class ComposeActivity : ) } - viewModel.sendStatus(contentText, spoilerText).observe( - this - ) { + lifecycleScope.launch { + viewModel.sendStatus(contentText, spoilerText) finishingUploadDialog?.dismiss() deleteDraftAndFinish() } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index a7e1779c..8829980b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -18,10 +18,7 @@ package com.keylesspalace.tusky.components.compose import android.net.Uri import android.util.Log import androidx.core.net.toUri -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel -import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia @@ -38,30 +35,34 @@ import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.service.ServiceClient import com.keylesspalace.tusky.service.StatusToSend -import com.keylesspalace.tusky.util.combineLiveData import com.keylesspalace.tusky.util.randomAlphanumericString -import com.keylesspalace.tusky.util.toLiveData -import io.reactivex.rxjava3.core.Observable import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.updateAndGet import kotlinx.coroutines.launch -import kotlinx.coroutines.rx3.rxSingle import kotlinx.coroutines.withContext import javax.inject.Inject +@OptIn(FlowPreview::class) class ComposeViewModel @Inject constructor( private val api: MastodonApi, private val accountManager: AccountManager, private val mediaUploader: MediaUploader, private val serviceClient: ServiceClient, private val draftHelper: DraftHelper, - private val instanceInfoRepo: InstanceInfoRepository + instanceInfoRepo: InstanceInfoRepository ) : ViewModel() { private var replyingStatusAuthor: String? = null @@ -76,40 +77,32 @@ class ComposeViewModel @Inject constructor( private var contentWarningStateChanged: Boolean = false private var modifiedInitialState: Boolean = false - val instanceInfo: MutableLiveData = MutableLiveData() + val instanceInfo: SharedFlow = instanceInfoRepo::getInstanceInfo.asFlow() + .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) - val emoji: MutableLiveData?> = MutableLiveData() - val markMediaAsSensitive = - mutableLiveData(accountManager.activeAccount?.defaultMediaSensitivity ?: false) + val emoji: SharedFlow> = instanceInfoRepo::getEmojis.asFlow() + .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) - val statusVisibility = mutableLiveData(Status.Visibility.UNKNOWN) - val showContentWarning = mutableLiveData(false) - val setupComplete = mutableLiveData(false) - val poll: MutableLiveData = mutableLiveData(null) - val scheduledAt: MutableLiveData = mutableLiveData(null) + val markMediaAsSensitive: MutableStateFlow = + MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false) + + val statusVisibility: MutableStateFlow = MutableStateFlow(Status.Visibility.UNKNOWN) + val showContentWarning: MutableStateFlow = MutableStateFlow(false) + val setupComplete: MutableStateFlow = MutableStateFlow(false) + val poll: MutableStateFlow = MutableStateFlow(null) + val scheduledAt: MutableStateFlow = MutableStateFlow(null) val media: MutableStateFlow> = MutableStateFlow(emptyList()) - val uploadError = MutableLiveData() + val uploadError = MutableSharedFlow(replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val mediaToJob = mutableMapOf() - private val isEditingScheduledToot get() = !scheduledTootId.isNullOrEmpty() - // Used in ComposeActivity to pass state to result function when cropImage contract inflight var cropImageItemOld: QueuedMedia? = null - init { - viewModelScope.launch { - emoji.postValue(instanceInfoRepo.getEmojis()) - } - viewModelScope.launch { - instanceInfo.postValue(instanceInfoRepo.getInstanceInfo()) - } - } - suspend fun pickMedia(mediaUri: Uri, description: String? = null): Result = withContext(Dispatchers.IO) { try { - val (type, uri, size) = mediaUploader.prepareMedia(mediaUri) + val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first()) val mediaItems = media.value if (type != QueuedMedia.Type.IMAGE && mediaItems.isNotEmpty() && @@ -157,10 +150,10 @@ class ComposeViewModel @Inject constructor( mediaToJob[mediaItem.localId] = viewModelScope.launch { mediaUploader - .uploadMedia(mediaItem) + .uploadMedia(mediaItem, instanceInfo.first()) .catch { error -> media.update { mediaValue -> mediaValue.filter { it.localId != mediaItem.localId } } - uploadError.postValue(error) + uploadError.emit(error) } .collect { event -> val item = media.value.find { it.localId == mediaItem.localId } @@ -216,7 +209,7 @@ class ComposeViewModel @Inject constructor( startingText?.startsWith(content.toString()) ?: false ) - val contentWarningChanged = showContentWarning.value!! && + val contentWarningChanged = showContentWarning.value && !contentWarning.isNullOrEmpty() && !startingContentWarning.startsWith(contentWarning.toString()) val mediaChanged = media.value.isNotEmpty() @@ -259,8 +252,8 @@ class ComposeViewModel @Inject constructor( inReplyToId = inReplyToId, content = content, contentWarning = contentWarning, - sensitive = markMediaAsSensitive.value!!, - visibility = statusVisibility.value!!, + sensitive = markMediaAsSensitive.value, + visibility = statusVisibility.value, mediaUris = mediaUris, mediaDescriptions = mediaDescriptions, poll = poll.value, @@ -271,38 +264,34 @@ class ComposeViewModel @Inject constructor( /** * Send status to the server. * Uses current state plus provided arguments. - * @return LiveData which will signal once the screen can be closed or null if there are errors */ - fun sendStatus( + suspend fun sendStatus( content: String, spoilerText: String - ): LiveData { + ) { - val deletionObservable = if (isEditingScheduledToot) { - rxSingle { api.deleteScheduledStatus(scheduledTootId.toString()) }.toObservable().map { } - } else { - Observable.just(Unit) - }.toLiveData() + if (!scheduledTootId.isNullOrEmpty()) { + api.deleteScheduledStatus(scheduledTootId!!) + } - val sendFlow = media + media .filter { items -> items.all { it.uploadPercent == -1 } } - .map { + .first { val mediaIds: MutableList = mutableListOf() val mediaUris: MutableList = mutableListOf() val mediaDescriptions: MutableList = mutableListOf() val mediaProcessed: MutableList = mutableListOf() - for (item in media.value) { + media.value.forEach { item -> mediaIds.add(item.id!!) mediaUris.add(item.uri) mediaDescriptions.add(item.description ?: "") mediaProcessed.add(false) } - val tootToSend = StatusToSend( text = content, warningText = spoilerText, - visibility = statusVisibility.value!!.serverString(), - sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value!! || showContentWarning.value!!), + visibility = statusVisibility.value.serverString(), + sensitive = mediaUris.isNotEmpty() && (markMediaAsSensitive.value || showContentWarning.value), mediaIds = mediaIds, mediaUris = mediaUris.map { it.toString() }, mediaDescriptions = mediaDescriptions, @@ -319,9 +308,8 @@ class ComposeViewModel @Inject constructor( ) serviceClient.sendToot(tootToSend) + true } - - return combineLiveData(deletionObservable, sendFlow.asLiveData()) { _, _ -> } } suspend fun updateDescription(localId: Int, description: String): Boolean { @@ -369,7 +357,7 @@ class ComposeViewModel @Inject constructor( }) } ':' -> { - val emojiList = emoji.value ?: return emptyList() + val emojiList = emoji.replayCache.firstOrNull() ?: return emptyList() val incomplete = token.substring(1) return emojiList.filter { emoji -> @@ -389,7 +377,7 @@ class ComposeViewModel @Inject constructor( fun setup(composeOptions: ComposeActivity.ComposeOptions?) { - if (setupComplete.value == true) { + if (setupComplete.value) { return } @@ -476,8 +464,6 @@ class ComposeViewModel @Inject constructor( } } -fun mutableLiveData(default: T) = MutableLiveData().apply { value = default } - /** * Thrown when trying to add an image when video is already present or the other way around */ diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt index 324540d1..221b306e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt @@ -27,6 +27,7 @@ import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.R import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo import com.keylesspalace.tusky.network.MediaUploadApi import com.keylesspalace.tusky.network.ProgressRequestBody import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN @@ -82,10 +83,10 @@ class MediaUploader @Inject constructor( ) { @OptIn(ExperimentalCoroutinesApi::class) - fun uploadMedia(media: QueuedMedia): Flow { + fun uploadMedia(media: QueuedMedia, instanceInfo: InstanceInfo): Flow { return flow { - if (shouldResizeMedia(media)) { - emit(downsize(media)) + if (shouldResizeMedia(media, instanceInfo)) { + emit(downsize(media, instanceInfo)) } else { emit(media) } @@ -94,7 +95,7 @@ class MediaUploader @Inject constructor( .flowOn(Dispatchers.IO) } - fun prepareMedia(inUri: Uri): PreparedMedia { + fun prepareMedia(inUri: Uri, instanceInfo: InstanceInfo): PreparedMedia { var mediaSize = MEDIA_SIZE_UNKNOWN var uri = inUri val mimeType: String? @@ -164,7 +165,7 @@ class MediaUploader @Inject constructor( if (mimeType != null) { return when (mimeType.substring(0, mimeType.indexOf('/'))) { "video" -> { - if (mediaSize > STATUS_VIDEO_SIZE_LIMIT) { + if (mediaSize > instanceInfo.videoSizeLimit) { throw VideoSizeException() } PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize) @@ -173,7 +174,7 @@ class MediaUploader @Inject constructor( PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize) } "audio" -> { - if (mediaSize > STATUS_AUDIO_SIZE_LIMIT) { + if (mediaSize > instanceInfo.videoSizeLimit) { throw AudioSizeException() } PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize) @@ -239,22 +240,18 @@ class MediaUploader @Inject constructor( } } - private fun downsize(media: QueuedMedia): QueuedMedia { + private fun downsize(media: QueuedMedia, instanceInfo: InstanceInfo): QueuedMedia { val file = createNewImageFile(context) - downsizeImage(media.uri, STATUS_IMAGE_SIZE_LIMIT, contentResolver, file) + downsizeImage(media.uri, instanceInfo.imageSizeLimit, contentResolver, file) return media.copy(uri = file.toUri(), mediaSize = file.length()) } - private fun shouldResizeMedia(media: QueuedMedia): Boolean { + private fun shouldResizeMedia(media: QueuedMedia, instanceInfo: InstanceInfo): Boolean { return media.type == QueuedMedia.Type.IMAGE && - (media.mediaSize > STATUS_IMAGE_SIZE_LIMIT || getImageSquarePixels(context.contentResolver, media.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT) + (media.mediaSize > instanceInfo.imageSizeLimit || getImageSquarePixels(context.contentResolver, media.uri) > instanceInfo.imageMatrixLimit) } private companion object { private const val TAG = "MediaUploader" - private const val STATUS_VIDEO_SIZE_LIMIT = 41943040 // 40MiB - private const val STATUS_AUDIO_SIZE_LIMIT = 41943040 // 40MiB - private const val STATUS_IMAGE_SIZE_LIMIT = 8388608 // 8MiB - private const val STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216 // 4096^2 Pixels } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt index 05e10b6b..bb97621c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt @@ -21,5 +21,12 @@ data class InstanceInfo( val pollMaxLength: Int, val pollMinDuration: Int, val pollMaxDuration: Int, - val charactersReservedPerUrl: Int + val charactersReservedPerUrl: Int, + val videoSizeLimit: Int, + val imageSizeLimit: Int, + val imageMatrixLimit: Int, + val maxMediaAttachments: Int, + val maxFields: Int, + val maxFieldNameLength: Int?, + val maxFieldValueLength: Int? ) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt index 20c44ba4..7415dd06 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt @@ -69,7 +69,14 @@ class InstanceInfoRepository @Inject constructor( minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration, maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration, charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl, - version = instance.version + version = instance.version, + videoSizeLimit = instance.configuration?.mediaAttachments?.videoSizeLimit, + imageSizeLimit = instance.configuration?.mediaAttachments?.imageSizeLimit, + imageMatrixLimit = instance.configuration?.mediaAttachments?.imageMatrixLimit, + maxMediaAttachments = instance.configuration?.statuses?.maxMediaAttachments ?: instance.maxMediaAttachments, + maxFields = instance.pleroma?.metadata?.fieldLimits?.maxFields, + maxFieldNameLength = instance.pleroma?.metadata?.fieldLimits?.nameLength, + maxFieldValueLength = instance.pleroma?.metadata?.fieldLimits?.valueLength ) dao.insertOrReplace(instanceEntity) instanceEntity @@ -85,7 +92,14 @@ class InstanceInfoRepository @Inject constructor( pollMaxLength = instanceInfo?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH, pollMinDuration = instanceInfo?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION, pollMaxDuration = instanceInfo?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION, - charactersReservedPerUrl = instanceInfo?.charactersReservedPerUrl ?: DEFAULT_CHARACTERS_RESERVED_PER_URL + charactersReservedPerUrl = instanceInfo?.charactersReservedPerUrl ?: DEFAULT_CHARACTERS_RESERVED_PER_URL, + videoSizeLimit = instanceInfo?.videoSizeLimit ?: DEFAULT_VIDEO_SIZE_LIMIT, + imageSizeLimit = instanceInfo?.imageSizeLimit ?: DEFAULT_IMAGE_SIZE_LIMIT, + imageMatrixLimit = instanceInfo?.imageMatrixLimit ?: DEFAULT_IMAGE_MATRIX_LIMIT, + maxMediaAttachments = instanceInfo?.maxMediaAttachments ?: DEFAULT_MAX_MEDIA_ATTACHMENTS, + maxFields = instanceInfo?.maxFields ?: DEFAULT_MAX_ACCOUNT_FIELDS, + maxFieldNameLength = instanceInfo?.maxFieldNameLength, + maxFieldValueLength = instanceInfo?.maxFieldValueLength ) } } @@ -99,7 +113,14 @@ class InstanceInfoRepository @Inject constructor( private const val DEFAULT_MIN_POLL_DURATION = 300 private const val DEFAULT_MAX_POLL_DURATION = 604800 + private const val DEFAULT_VIDEO_SIZE_LIMIT = 41943040 // 40MiB + private const val DEFAULT_IMAGE_SIZE_LIMIT = 10485760 // 10MiB + private const val DEFAULT_IMAGE_MATRIX_LIMIT = 16777216 // 4096^2 Pixels + // Mastodon only counts URLs as this long in terms of status character limits const val DEFAULT_CHARACTERS_RESERVED_PER_URL = 23 + + const val DEFAULT_MAX_MEDIA_ATTACHMENTS = 4 + const val DEFAULT_MAX_ACCOUNT_FIELDS = 4 } } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index c43b3652..e87758ce 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -31,7 +31,7 @@ import java.io.File; */ @Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, TimelineAccountEntity.class, ConversationEntity.class - }, version = 39) + }, version = 40) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -581,4 +581,17 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `clientSecret` TEXT"); } }; + + public static final Migration MIGRATION_39_40 = new Migration(39, 40) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `videoSizeLimit` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `imageSizeLimit` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `imageMatrixLimit` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxMediaAttachments` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxFields` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxFieldNameLength` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxFieldValueLength` INTEGER"); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt index 01767f32..efcfe527 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt @@ -31,7 +31,14 @@ data class InstanceEntity( val minPollDuration: Int?, val maxPollDuration: Int?, val charactersReservedPerUrl: Int?, - val version: String? + val version: String?, + val videoSizeLimit: Int?, + val imageSizeLimit: Int?, + val imageMatrixLimit: Int?, + val maxMediaAttachments: Int?, + val maxFields: Int?, + val maxFieldNameLength: Int?, + val maxFieldValueLength: Int? ) @TypeConverters(Converters::class) @@ -48,5 +55,12 @@ data class InstanceInfoEntity( val minPollDuration: Int?, val maxPollDuration: Int?, val charactersReservedPerUrl: Int?, - val version: String? + val version: String?, + val videoSizeLimit: Int?, + val imageSizeLimit: Int?, + val imageMatrixLimit: Int?, + val maxMediaAttachments: Int?, + val maxFields: Int?, + val maxFieldNameLength: Int?, + val maxFieldValueLength: Int? ) diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index e17cb3cf..b92dcf15 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -65,7 +65,7 @@ class AppModule { AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32, AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35, AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38, - AppDatabase.MIGRATION_38_39 + AppDatabase.MIGRATION_38_39, AppDatabase.MIGRATION_39_40 ) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt index 31fcca0e..eccc8696 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt @@ -19,19 +19,20 @@ import com.google.gson.annotations.SerializedName data class Instance( val uri: String, - val title: String, - val description: String, - val email: String, + // val title: String, + // val description: String, + // val email: String, val version: String, - val urls: Map, - val stats: Map?, - val thumbnail: String?, - val languages: List, - @SerializedName("contact_account") val contactAccount: Account, + // val urls: Map, + // val stats: Map?, + // val thumbnail: String?, + // val languages: List, + // @SerializedName("contact_account") val contactAccount: Account, @SerializedName("max_toot_chars") val maxTootChars: Int?, - @SerializedName("max_bio_chars") val maxBioChars: Int?, @SerializedName("poll_limits") val pollConfiguration: PollConfiguration?, val configuration: InstanceConfiguration?, + @SerializedName("max_media_attachments") val maxMediaAttachments: Int?, + val pleroma: PleromaConfiguration? ) { override fun hashCode(): Int { return uri.hashCode() @@ -74,3 +75,17 @@ data class MediaAttachmentConfiguration( @SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int?, @SerializedName("video_matrix_limit") val videoMatrixLimit: Int?, ) + +data class PleromaConfiguration( + val metadata: PleromaMetadata? +) + +data class PleromaMetadata( + @SerializedName("fields_limits") val fieldLimits: PleromaFieldLimits +) + +data class PleromaFieldLimits( + @SerializedName("max_fields") val maxFields: Int?, + @SerializedName("name_length") val nameLength: Int?, + @SerializedName("value_length") val valueLength: Int? +) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt b/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt deleted file mode 100644 index 21c4307c..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/LiveDataUtil.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* Copyright 2019 Tusky Contributors - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.util - -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.LiveDataReactiveStreams -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.Observer -import androidx.lifecycle.Transformations -import io.reactivex.rxjava3.core.BackpressureStrategy -import io.reactivex.rxjava3.core.Observable -import io.reactivex.rxjava3.core.Single - -inline fun LiveData.map(crossinline mapFunction: (X) -> Y): LiveData = - Transformations.map(this) { input -> mapFunction(input) } - -inline fun LiveData.switchMap( - crossinline switchMapFunction: (X) -> LiveData -): LiveData = Transformations.switchMap(this) { input -> switchMapFunction(input) } - -inline fun LiveData.filter(crossinline predicate: (X) -> Boolean): LiveData { - val liveData = MediatorLiveData() - liveData.addSource(this) { value -> - if (predicate(value)) { - liveData.value = value - } - } - return liveData -} - -fun LifecycleOwner.withLifecycleContext(body: LifecycleContext.() -> Unit) = - LifecycleContext(this).apply(body) - -class LifecycleContext(val lifecycleOwner: LifecycleOwner) { - inline fun LiveData.observe(crossinline observer: (T) -> Unit) = - this.observe(lifecycleOwner, Observer { observer(it) }) - - /** - * Just hold a subscription, - */ - fun LiveData.subscribe() = - this.observe(lifecycleOwner, Observer { }) -} - -/** - * Invokes @param [combiner] when value of both @param [a] and @param [b] are not null. Returns - * [LiveData] with value set to the result of calling [combiner] with value of both. - * Important! You still need to observe to the returned [LiveData] for [combiner] to be invoked. - */ -fun combineLiveData(a: LiveData, b: LiveData, combiner: (A, B) -> R): LiveData { - val liveData = MediatorLiveData() - liveData.addSource(a) { - if (a.value != null && b.value != null) { - liveData.value = combiner(a.value!!, b.value!!) - } - } - liveData.addSource(b) { - if (a.value != null && b.value != null) { - liveData.value = combiner(a.value!!, b.value!!) - } - } - return liveData -} - -/** - * Returns [LiveData] with value set to the result of calling [combiner] with value of [a] and [b] - * after either changes. Doesn't check if either has value. - * Important! You still need to observe to the returned [LiveData] for [combiner] to be invoked. - */ -fun combineOptionalLiveData(a: LiveData, b: LiveData, combiner: (A?, B?) -> R): LiveData { - val liveData = MediatorLiveData() - liveData.addSource(a) { - liveData.value = combiner(a.value, b.value) - } - liveData.addSource(b) { - liveData.value = combiner(a.value, b.value) - } - return liveData -} - -fun Single.toLiveData() = LiveDataReactiveStreams.fromPublisher(this.toFlowable()) -fun Observable.toLiveData( - backpressureStrategy: BackpressureStrategy = BackpressureStrategy.LATEST -) = LiveDataReactiveStreams.fromPublisher(this.toFlowable(BackpressureStrategy.LATEST)) diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt index bc7f435d..88776606 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -24,8 +24,9 @@ import androidx.lifecycle.viewModelScope import at.connyduck.calladapter.networkresult.fold import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.ProfileEditedEvent +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository import com.keylesspalace.tusky.entity.Account -import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.StringField import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.Error @@ -34,6 +35,11 @@ import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.Success import com.keylesspalace.tusky.util.getServerErrorMessage import com.keylesspalace.tusky.util.randomAlphanumericString +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.launch import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody @@ -49,14 +55,18 @@ private const val AVATAR_FILE_NAME = "avatar.png" class EditProfileViewModel @Inject constructor( private val mastodonApi: MastodonApi, private val eventHub: EventHub, - private val application: Application + private val application: Application, + private val instanceInfoRepo: InstanceInfoRepository ) : ViewModel() { val profileData = MutableLiveData>() val avatarData = MutableLiveData() val headerData = MutableLiveData() val saveData = MutableLiveData>() - val instanceData = MutableLiveData>() + + @OptIn(FlowPreview::class) + val instanceData: Flow = instanceInfoRepo::getInstanceInfo.asFlow() + .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) private var oldProfileData: Account? = null @@ -186,19 +196,4 @@ class EditProfileViewModel @Inject constructor( private fun getCacheFileForName(filename: String): File { return File(application.cacheDir, filename) } - - fun obtainInstance() = viewModelScope.launch { - if (instanceData.value == null || instanceData.value is Error) { - instanceData.postValue(Loading()) - - mastodonApi.getInstance().fold( - { instance -> - instanceData.postValue(Success(instance)) - }, - { - instanceData.postValue(Error()) - } - ) - } - } } diff --git a/app/src/main/res/layout/item_edit_field.xml b/app/src/main/res/layout/item_edit_field.xml index 2777730b..a658343e 100644 --- a/app/src/main/res/layout/item_edit_field.xml +++ b/app/src/main/res/layout/item_edit_field.xml @@ -1,5 +1,6 @@ - + app:counterTextColor="?android:textColorTertiary"> - + + + + + app:counterTextColor="?android:textColorTertiary"> + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index ef863560..f041f57b 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -30,7 +30,6 @@ import com.keylesspalace.tusky.db.EmojisEntity import com.keylesspalace.tusky.db.InstanceDao import com.keylesspalace.tusky.db.InstanceInfoEntity import com.keylesspalace.tusky.di.ViewModelFactory -import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Instance import com.keylesspalace.tusky.entity.InstanceConfiguration import com.keylesspalace.tusky.entity.StatusConfiguration @@ -48,8 +47,6 @@ import org.robolectric.Robolectric import org.robolectric.Shadows.shadowOf import org.robolectric.annotation.Config import org.robolectric.fakes.RoboMenuItem -import java.util.Date -import kotlin.collections.HashMap /** * Created by charlag on 3/7/18. @@ -110,7 +107,7 @@ class ComposeActivityTest { val instanceDaoMock: InstanceDao = mock { onBlocking { getInstanceInfo(any()) } doReturn - InstanceInfoEntity(instanceDomain, null, null, null, null, null, null, null) + InstanceInfoEntity(instanceDomain, null, null, null, null, null, null, null, null, null, null, null, null, null, null) onBlocking { getEmojiInfo(any()) } doReturn EmojisEntity(instanceDomain, emptyList()) } @@ -461,38 +458,13 @@ class ComposeActivityTest { private fun getInstanceWithCustomConfiguration(maximumLegacyTootCharacters: Int? = null, configuration: InstanceConfiguration? = null): Instance { return Instance( - "https://example.token", - "Example dot Token", - "Example instance for testing", - "admin@example.token", - "2.6.3", - HashMap(), - null, - null, - listOf("en"), - Account( - id = "1", - localUsername = "admin", - username = "admin", - displayName = "admin", - createdAt = Date(), - note = "", - url = "https://example.token", - avatar = "", - header = "", - locked = false, - statusesCount = 0, - followersCount = 0, - followingCount = 0, - source = null, - bot = false, - emojis = emptyList(), - fields = emptyList(), - ), - maximumLegacyTootCharacters, - null, - null, - configuration, + uri = "https://example.token", + version = "2.6.3", + maxTootChars = maximumLegacyTootCharacters, + pollConfiguration = null, + configuration = configuration, + maxMediaAttachments = null, + pleroma = null ) }