diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/29.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/29.json index ac6eb677d..654211ae0 100644 --- a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/29.json +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/29.json @@ -786,4 +786,4 @@ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '62c289344334da2db091ad4ba0a49c6a')" ] } -} \ No newline at end of file +} diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/30.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/30.json new file mode 100644 index 000000000..e0a9e9f6d --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/30.json @@ -0,0 +1,807 @@ +{ + "formatVersion": 1, + "database": { + "version": 30, + "identityHash": "a75615171612bdfc9e3d4201ebf6071a", + "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, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a75615171612bdfc9e3d4201ebf6071a')" + ] + } +} \ No newline at end of file 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 fef8f9326..d1ae0b9e3 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 com.keylesspalace.tusky.appstore.AnnouncementReadEvent import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.db.InstanceEntity @@ -57,19 +58,21 @@ class AnnouncementsViewModel @Inject constructor( .onErrorResumeNext { mastodonApi.getInstance() .map { Either.Right(it) } - }, - { emojis, either -> - either.asLeftOrNull()?.copy(emojiList = emojis) - ?: InstanceEntity( - accountManager.activeAccount?.domain!!, - emojis, - either.asRight().maxTootChars, - either.asRight().pollLimits?.maxOptions, - either.asRight().pollLimits?.maxOptionChars, - either.asRight().version - ) - } - ) + } + ) { emojis, either -> + either.asLeftOrNull()?.copy(emojiList = emojis) + ?: InstanceEntity( + accountManager.activeAccount?.domain!!, + emojis, + either.asRight().configuration?.statuses?.maxCharacters ?: either.asRight().maxTootChars, + either.asRight().configuration?.polls?.maxOptions ?: either.asRight().pollConfiguration?.maxOptions, + either.asRight().configuration?.polls?.maxCharactersPerOption ?: either.asRight().pollConfiguration?.maxOptionChars, + either.asRight().configuration?.polls?.minExpiration ?: either.asRight().pollConfiguration?.minExpiration, + either.asRight().configuration?.polls?.maxExpiration ?: either.asRight().pollConfiguration?.maxExpiration, + either.asRight().configuration?.statuses?.charactersReservedPerUrl ?: DEFAULT_MAXIMUM_URL_LENGTH, + either.asRight().version + ) + } .doOnSuccess { appDatabase.instanceDao().insertOrReplace(it) } 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 bfce01304..caee042fd 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 @@ -122,6 +122,7 @@ class ComposeActivity : @VisibleForTesting var maximumTootCharacters = DEFAULT_CHARACTER_LIMIT + var charactersReservedPerUrl = DEFAULT_MAXIMUM_URL_LENGTH private val viewModel: ComposeViewModel by viewModels { viewModelFactory } @@ -316,6 +317,7 @@ class ComposeActivity : withLifecycleContext { viewModel.instanceParams.observe { instanceData -> maximumTootCharacters = instanceData.maxChars + charactersReservedPerUrl = instanceData.charactersReservedPerUrl updateVisibleCharactersLeft() binding.composeScheduleButton.visible(instanceData.supportsScheduled) } @@ -654,7 +656,8 @@ class ComposeActivity : val instanceParams = viewModel.instanceParams.value!! showAddPollDialog( this, viewModel.poll.value, instanceParams.pollMaxOptions, - instanceParams.pollMaxLength, viewModel::updatePoll + instanceParams.pollMaxLength, instanceParams.pollMinDuration, instanceParams.pollMaxDuration, + viewModel::updatePoll ) } @@ -699,7 +702,7 @@ class ComposeActivity : val urlSpans = binding.composeEditField.urls if (urlSpans != null) { for (span in urlSpans) { - offset += max(0, span.url.length - MAXIMUM_URL_LENGTH) + offset += max(0, span.url.length - charactersReservedPerUrl) } } var length = binding.composeEditField.length() - offset @@ -1042,10 +1045,6 @@ class ComposeActivity : internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS" private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI" - // Mastodon only counts URLs as this long in terms of status character limits - @VisibleForTesting - const val MAXIMUM_URL_LENGTH = 23 - @JvmStatic fun startIntent(context: Context, options: ComposeOptions): Intent { return Intent(context, ComposeActivity::class.java).apply { 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 2eb5b9fcd..8eda55510 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 @@ -79,6 +79,9 @@ class ComposeViewModel @Inject constructor( maxChars = instance?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT, pollMaxOptions = instance?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT, pollMaxLength = instance?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH, + pollMinDuration = instance?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION, + pollMaxDuration = instance?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION, + charactersReservedPerUrl = instance?.charactersReservedPerUrl ?: DEFAULT_MAXIMUM_URL_LENGTH, supportsScheduled = instance?.version?.let { VersionUtils(it).supportsScheduledToots() } ?: false ) } @@ -102,18 +105,20 @@ class ComposeViewModel @Inject constructor( init { Single.zip( - api.getCustomEmojis(), api.getInstance(), - { emojis, instance -> - InstanceEntity( - instance = accountManager.activeAccount?.domain!!, - emojiList = emojis, - maximumTootCharacters = instance.maxTootChars, - maxPollOptions = instance.pollLimits?.maxOptions, - maxPollOptionLength = instance.pollLimits?.maxOptionChars, - version = instance.version - ) - } - ) + api.getCustomEmojis(), api.getInstance() + ) { emojis, instance -> + InstanceEntity( + instance = accountManager.activeAccount?.domain!!, + emojiList = emojis, + maximumTootCharacters = instance.configuration?.statuses?.maxCharacters ?: instance.maxTootChars, + maxPollOptions = instance.configuration?.polls?.maxOptions ?: instance.pollConfiguration?.maxOptions, + maxPollOptionLength = instance.configuration?.polls?.maxCharactersPerOption ?: instance.pollConfiguration?.maxOptionChars, + minPollDuration = instance.configuration?.polls?.minExpiration ?: instance.pollConfiguration?.minExpiration, + maxPollDuration = instance.configuration?.polls?.maxExpiration ?: instance.pollConfiguration?.maxExpiration, + charactersReservedPerUrl = instance.configuration?.statuses?.charactersReservedPerUrl, + version = instance.version + ) + } .doOnSuccess { db.instanceDao().insertOrReplace(it) } @@ -506,11 +511,19 @@ fun mutableLiveData(default: T) = MutableLiveData().apply { value = defau const val DEFAULT_CHARACTER_LIMIT = 500 private const val DEFAULT_MAX_OPTION_COUNT = 4 private const val DEFAULT_MAX_OPTION_LENGTH = 50 +private const val DEFAULT_MIN_POLL_DURATION = 300 +private const val DEFAULT_MAX_POLL_DURATION = 604800 + +// Mastodon only counts URLs as this long in terms of status character limits +const val DEFAULT_MAXIMUM_URL_LENGTH = 23 data class ComposeInstanceParams( val maxChars: Int, val pollMaxOptions: Int, val pollMaxLength: Int, + val pollMinDuration: Int, + val pollMaxDuration: Int, + val charactersReservedPerUrl: Int, val supportsScheduled: Boolean ) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt index 7a4f73898..a87b1b238 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt @@ -20,6 +20,7 @@ package com.keylesspalace.tusky.components.compose.dialog import android.content.Context import android.view.LayoutInflater import android.view.WindowManager +import android.widget.ArrayAdapter import androidx.appcompat.app.AlertDialog import com.keylesspalace.tusky.R import com.keylesspalace.tusky.databinding.DialogAddPollBinding @@ -30,6 +31,8 @@ fun showAddPollDialog( poll: NewPoll?, maxOptionCount: Int, maxOptionLength: Int, + minDuration: Int, + maxDuration: Int, onUpdatePoll: (NewPoll) -> Unit ) { @@ -57,6 +60,13 @@ fun showAddPollDialog( binding.pollChoices.adapter = adapter + var durations = context.resources.getIntArray(R.array.poll_duration_values).toList() + val durationLabels = context.resources.getStringArray(R.array.poll_duration_names).filterIndexed { index, _ -> durations[index] in minDuration..maxDuration } + binding.pollDurationSpinner.adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, durationLabels).apply { + setDropDownViewResource(R.layout.support_simple_spinner_dropdown_item) + } + durations = durations.filter { it in minDuration..maxDuration } + binding.addChoiceButton.setOnClickListener { if (adapter.itemCount < maxOptionCount) { adapter.addChoice() @@ -66,7 +76,7 @@ fun showAddPollDialog( } } - val pollDurationId = context.resources.getIntArray(R.array.poll_duration_values).indexOfLast { + val pollDurationId = durations.indexOfLast { it <= poll?.expiresIn ?: 0 } @@ -79,13 +89,10 @@ fun showAddPollDialog( button.setOnClickListener { val selectedPollDurationId = binding.pollDurationSpinner.selectedItemPosition - val pollDuration = context.resources - .getIntArray(R.array.poll_duration_values)[selectedPollDurationId] - onUpdatePoll( NewPoll( options = adapter.pollOptions, - expiresIn = pollDuration, + expiresIn = durations[selectedPollDurationId], multiple = binding.multipleChoicesCheckBox.isChecked ) ) 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 b97b82542..0ab627d54 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -32,7 +32,7 @@ import java.io.File; @Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, TimelineAccountEntity.class, ConversationEntity.class - }, version = 29) + }, version = 30) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -465,4 +465,13 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `tags` TEXT"); } }; + + public static final Migration MIGRATION_29_30 = new Migration(29, 30) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `charactersReservedPerUrl` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `minPollDuration` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollDuration` 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 ac4464f2d..dd8e85d07 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/InstanceEntity.kt @@ -28,5 +28,8 @@ data class InstanceEntity( val maximumTootCharacters: Int?, val maxPollOptions: Int?, val maxPollOptionLength: Int?, + val minPollDuration: Int?, + val maxPollDuration: Int?, + val charactersReservedPerUrl: Int?, val version: String? ) diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index 583b5ad6b..fb0bd650e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -61,7 +61,8 @@ class AppModule { AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22, AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25, AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")), - AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29 + AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29, + AppDatabase.MIGRATION_29_30 ) .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 d1e2aca90..31fcca0e0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt @@ -30,7 +30,8 @@ data class Instance( @SerializedName("contact_account") val contactAccount: Account, @SerializedName("max_toot_chars") val maxTootChars: Int?, @SerializedName("max_bio_chars") val maxBioChars: Int?, - @SerializedName("poll_limits") val pollLimits: PollLimits? + @SerializedName("poll_limits") val pollConfiguration: PollConfiguration?, + val configuration: InstanceConfiguration?, ) { override fun hashCode(): Int { return uri.hashCode() @@ -45,7 +46,31 @@ data class Instance( } } -data class PollLimits( +data class PollConfiguration( @SerializedName("max_options") val maxOptions: Int?, - @SerializedName("max_option_chars") val maxOptionChars: Int? + @SerializedName("max_option_chars") val maxOptionChars: Int?, + @SerializedName("max_characters_per_option") val maxCharactersPerOption: Int?, + @SerializedName("min_expiration") val minExpiration: Int?, + @SerializedName("max_expiration") val maxExpiration: Int?, +) + +data class InstanceConfiguration( + val statuses: StatusConfiguration?, + @SerializedName("media_attachments") val mediaAttachments: MediaAttachmentConfiguration?, + val polls: PollConfiguration?, +) + +data class StatusConfiguration( + @SerializedName("max_characters") val maxCharacters: Int?, + @SerializedName("max_media_attachments") val maxMediaAttachments: Int?, + @SerializedName("characters_reserved_per_url") val charactersReservedPerUrl: Int?, +) + +data class MediaAttachmentConfiguration( + @SerializedName("supported_mime_types") val supportedMimeTypes: List?, + @SerializedName("image_size_limit") val imageSizeLimit: Int?, + @SerializedName("image_matrix_limit") val imageMatrixLimit: Int?, + @SerializedName("video_size_limit") val videoSizeLimit: Int?, + @SerializedName("video_frame_rate_limit") val videoFrameRateLimit: Int?, + @SerializedName("video_matrix_limit") val videoMatrixLimit: Int?, ) diff --git a/app/src/main/res/layout/dialog_add_poll.xml b/app/src/main/res/layout/dialog_add_poll.xml index ed8abc076..4ba72250b 100644 --- a/app/src/main/res/layout/dialog_add_poll.xml +++ b/app/src/main/res/layout/dialog_add_poll.xml @@ -37,7 +37,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="8dp" - android:entries="@array/poll_duration_names" app:layout_constraintBottom_toBottomOf="@id/addChoiceButton" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/addChoiceButton" diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index efc898d53..8016e2c43 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -164,6 +164,12 @@ @string/duration_1_day @string/duration_3_days @string/duration_7_days + @string/duration_14_days + @string/duration_30_days + @string/duration_60_days + @string/duration_90_days + @string/duration_180_days + @string/duration_365_days @@ -174,6 +180,12 @@ 86400 259200 604800 + 1209600 + 2592000 + 5184000 + 7776000 + 15552000 + 31536000 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 205e5dba5..74870854d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -580,6 +580,12 @@ 1 day 3 days 7 days + 14 days + 30 days + 60 days + 90 days + 180 days + 365 days Add choice Multiple choices Choice %d diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index 156a6b413..410522324 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -23,6 +23,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.keylesspalace.tusky.components.compose.ComposeActivity import com.keylesspalace.tusky.components.compose.ComposeViewModel import com.keylesspalace.tusky.components.compose.DEFAULT_CHARACTER_LIMIT +import com.keylesspalace.tusky.components.compose.DEFAULT_MAXIMUM_URL_LENGTH import com.keylesspalace.tusky.components.compose.MediaUploader import com.keylesspalace.tusky.components.drafts.DraftHelper import com.keylesspalace.tusky.db.AccountEntity @@ -33,6 +34,8 @@ import com.keylesspalace.tusky.db.InstanceEntity import com.keylesspalace.tusky.di.ViewModelFactory import com.keylesspalace.tusky.entity.Account import com.keylesspalace.tusky.entity.Instance +import com.keylesspalace.tusky.entity.InstanceConfiguration +import com.keylesspalace.tusky.entity.StatusConfiguration import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.service.ServiceClient import com.nhaarman.mockitokotlin2.any @@ -109,7 +112,7 @@ class ComposeActivityTest { val instanceDaoMock = mock(InstanceDao::class.java) `when`(instanceDaoMock.loadMetadataForInstance(any())).thenReturn( - Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null)) + Single.just(InstanceEntity(instanceDomain, emptyList(), null, null, null, null, null, null, null)) ) val dbMock = mock(AppDatabase::class.java) @@ -182,7 +185,7 @@ class ComposeActivityTest { @Test fun whenMaximumTootCharsIsNull_defaultLimitIsUsed() { - instanceResponseCallback = { getInstanceWithMaximumTootCharacters(null) } + instanceResponseCallback = { getInstanceWithCustomConfiguration(null) } setupActivity() assertEquals(DEFAULT_CHARACTER_LIMIT, activity.maximumTootCharacters) } @@ -190,12 +193,39 @@ class ComposeActivityTest { @Test fun whenMaximumTootCharsIsPopulated_customLimitIsUsed() { val customMaximum = 1000 - instanceResponseCallback = { getInstanceWithMaximumTootCharacters(customMaximum) } + instanceResponseCallback = { getInstanceWithCustomConfiguration(customMaximum, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum)) } setupActivity() shadowOf(getMainLooper()).idle() assertEquals(customMaximum, activity.maximumTootCharacters) } + @Test + fun whenOnlyLegacyMaximumTootCharsIsPopulated_customLimitIsUsed() { + val customMaximum = 1000 + instanceResponseCallback = { getInstanceWithCustomConfiguration(customMaximum) } + setupActivity() + shadowOf(getMainLooper()).idle() + assertEquals(customMaximum, activity.maximumTootCharacters) + } + + @Test + fun whenOnlyConfigurationMaximumTootCharsIsPopulated_customLimitIsUsed() { + val customMaximum = 1000 + instanceResponseCallback = { getInstanceWithCustomConfiguration(null, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum)) } + setupActivity() + shadowOf(getMainLooper()).idle() + assertEquals(customMaximum, activity.maximumTootCharacters) + } + + @Test + fun whenDifferentCharLimitsArePopulated_statusConfigurationLimitIsUsed() { + val customMaximum = 1000 + instanceResponseCallback = { getInstanceWithCustomConfiguration(customMaximum, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum * 2)) } + setupActivity() + shadowOf(getMainLooper()).idle() + assertEquals(customMaximum * 2, activity.maximumTootCharacters) + } + @Test fun whenTextContainsNoUrl_everyCharacterIsCounted() { val content = "This is test content please ignore thx " @@ -208,7 +238,7 @@ class ComposeActivityTest { val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:" val additionalContent = "Check out this @image #search result: " insertSomeTextInContent(additionalContent + url) - assertEquals(activity.calculateTextLength(), additionalContent.length + ComposeActivity.MAXIMUM_URL_LENGTH) + assertEquals(activity.calculateTextLength(), additionalContent.length + DEFAULT_MAXIMUM_URL_LENGTH) } @Test @@ -217,7 +247,7 @@ class ComposeActivityTest { val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:" val additionalContent = " Check out this @image #search result: " insertSomeTextInContent(shortUrl + additionalContent + url) - assertEquals(activity.calculateTextLength(), additionalContent.length + shortUrl.length + ComposeActivity.MAXIMUM_URL_LENGTH) + assertEquals(activity.calculateTextLength(), additionalContent.length + shortUrl.length + DEFAULT_MAXIMUM_URL_LENGTH) } @Test @@ -225,7 +255,44 @@ class ComposeActivityTest { val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:" val additionalContent = " Check out this @image #search result: " insertSomeTextInContent(url + additionalContent + url) - assertEquals(activity.calculateTextLength(), additionalContent.length + (ComposeActivity.MAXIMUM_URL_LENGTH * 2)) + assertEquals(activity.calculateTextLength(), additionalContent.length + (DEFAULT_MAXIMUM_URL_LENGTH * 2)) + } + + @Test + fun whenTextContainsUrl_onlyEllipsizedURLIsCounted_withCustomConfiguration() { + val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:" + val additionalContent = "Check out this @image #search result: " + val customUrlLength = 16 + instanceResponseCallback = { getInstanceWithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) } + setupActivity() + shadowOf(getMainLooper()).idle() + insertSomeTextInContent(additionalContent + url) + assertEquals(activity.calculateTextLength(), additionalContent.length + customUrlLength) + } + + @Test + fun whenTextContainsMultipleUrls_onlyEllipsizedURLIsCounted_withCustomConfiguration() { + val shortUrl = "https://tusky.app" + val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:" + val additionalContent = " Check out this @image #search result: " + val customUrlLength = 18 // The intention is that this is longer than shortUrl.length + instanceResponseCallback = { getInstanceWithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) } + setupActivity() + shadowOf(getMainLooper()).idle() + insertSomeTextInContent(shortUrl + additionalContent + url) + assertEquals(activity.calculateTextLength(), additionalContent.length + shortUrl.length + customUrlLength) + } + + @Test + fun whenTextContainsMultipleURLs_allURLsGetEllipsized_withCustomConfiguration() { + val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM:" + val additionalContent = " Check out this @image #search result: " + val customUrlLength = 16 + instanceResponseCallback = { getInstanceWithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) } + setupActivity() + shadowOf(getMainLooper()).idle() + insertSomeTextInContent(url + additionalContent + url) + assertEquals(activity.calculateTextLength(), additionalContent.length + (customUrlLength * 2)) } @Test @@ -387,7 +454,7 @@ class ComposeActivityTest { activity.findViewById(R.id.composeEditField).setText(text ?: "Some text") } - private fun getInstanceWithMaximumTootCharacters(maximumTootCharacters: Int?): Instance { + private fun getInstanceWithCustomConfiguration(maximumLegacyTootCharacters: Int? = null, configuration: InstanceConfiguration? = null): Instance { return Instance( "https://example.token", "Example dot Token", @@ -416,9 +483,22 @@ class ComposeActivityTest { emptyList(), emptyList() ), - maximumTootCharacters, + maximumLegacyTootCharacters, null, - null + null, + configuration, + ) + } + + fun getCustomInstanceConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): InstanceConfiguration { + return InstanceConfiguration( + statuses = StatusConfiguration( + maxCharacters = maximumStatusCharacters, + maxMediaAttachments = null, + charactersReservedPerUrl = charactersReservedPerUrl + ), + mediaAttachments = null, + polls = null ) } }