diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json new file mode 100644 index 000000000..e6d8ec7dc --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json @@ -0,0 +1,809 @@ +{ + "formatVersion": 1, + "database": { + "version": 33, + "identityHash": "920a0e0c9a600bd236f6bf959b469c18", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '920a0e0c9a600bd236f6bf959b469c18')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt index c218e1114..114a6cd0b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt @@ -78,7 +78,7 @@ import com.keylesspalace.tusky.util.emojify import com.keylesspalace.tusky.util.getDomain import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.loadAvatar -import com.keylesspalace.tusky.util.openLink +import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.setClickableText import com.keylesspalace.tusky.util.show import com.keylesspalace.tusky.util.viewBinding @@ -375,12 +375,11 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI } } viewModel.accountFieldData.observe( - this, - { - accountFieldAdapter.fields = it - accountFieldAdapter.notifyDataSetChanged() - } - ) + this + ) { + accountFieldAdapter.fields = it + accountFieldAdapter.notifyDataSetChanged() + } viewModel.noteSaved.observe(this) { binding.saveNoteInfo.visible(it, View.INVISIBLE) } @@ -395,11 +394,10 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI adapter.refreshContent() } viewModel.isRefreshing.observe( - this, - { isRefreshing -> - binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true - } - ) + this + ) { isRefreshing -> + binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true + } binding.swipeToRefreshLayout.setColorSchemeResources(R.color.tusky_blue) } @@ -410,7 +408,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasAndroidI binding.accountUsernameTextView.text = usernameFormatted binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis) - val emojifiedNote = account.note.emojify(account.emojis, binding.accountNoteTextView, animateEmojis) + val emojifiedNote = account.note.parseAsMastodonHtml().emojify(account.emojis, binding.accountNoteTextView, animateEmojis) setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this) // accountFieldAdapter.fields = account.fields ?: emptyList() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt index 093dbcfb5..d51bb1452 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt @@ -29,6 +29,7 @@ import com.keylesspalace.tusky.util.BindingHolder import com.keylesspalace.tusky.util.Either import com.keylesspalace.tusky.util.createClickableText import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.parseAsMastodonHtml import com.keylesspalace.tusky.util.setClickableText class AccountFieldAdapter( @@ -65,7 +66,7 @@ class AccountFieldAdapter( val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis) nameTextView.text = emojifiedName - val emojifiedValue = field.value.emojify(emojis, valueTextView, animateEmojis) + val emojifiedValue = field.value.parseAsMastodonHtml().emojify(emojis, valueTextView, animateEmojis) setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener) if (field.verifiedAt != null) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt index 89c1ad0f1..0c9465142 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt @@ -26,7 +26,7 @@ import com.keylesspalace.tusky.util.StatusDisplayOptions class ConversationAdapter( private val statusDisplayOptions: StatusDisplayOptions, private val listener: StatusActionListener -) : PagingDataAdapter(CONVERSATION_COMPARATOR) { +) : PagingDataAdapter(CONVERSATION_COMPARATOR) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder { val view = LayoutInflater.from(parent.context).inflate(R.layout.item_conversation, parent, false) @@ -37,17 +37,13 @@ class ConversationAdapter( holder.setupWithConversation(getItem(position)) } - fun item(position: Int): ConversationEntity? { - return getItem(position) - } - companion object { - val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean { + val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean { return oldItem.id == newItem.id } - override fun areContentsTheSame(oldItem: ConversationEntity, newItem: ConversationEntity): Boolean { + override fun areContentsTheSame(oldItem: ConversationViewData, newItem: ConversationViewData): Boolean { return oldItem == newItem } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt index 88c9dbad1..f585b4ea5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt @@ -15,7 +15,6 @@ package com.keylesspalace.tusky.components.conversation -import android.text.Spanned import androidx.room.Embedded import androidx.room.Entity import androidx.room.TypeConverters @@ -27,7 +26,7 @@ import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.TimelineAccount -import com.keylesspalace.tusky.util.shouldTrimStatus +import com.keylesspalace.tusky.viewdata.StatusViewData import java.util.Date @Entity(primaryKeys = ["id", "accountId"]) @@ -38,7 +37,16 @@ data class ConversationEntity( val accounts: List, val unread: Boolean, @Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity -) +) { + fun toViewData(): ConversationViewData { + return ConversationViewData( + id = id, + accounts = accounts, + unread = unread, + lastStatus = lastStatus.toViewData() + ) + } +} data class ConversationAccountEntity( val id: String, @@ -67,7 +75,7 @@ data class ConversationStatusEntity( val inReplyToId: String?, val inReplyToAccountId: String?, val account: ConversationAccountEntity, - val content: Spanned, + val content: String, val createdAt: Date, val emojis: List, val favouritesCount: Int, @@ -80,95 +88,43 @@ data class ConversationStatusEntity( val tags: List?, val showingHiddenContent: Boolean, val expanded: Boolean, - val collapsible: Boolean, val collapsed: Boolean, val muted: Boolean, val poll: Poll? ) { - /** its necessary to override this because Spanned.equals does not work as expected */ - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as ConversationStatusEntity - - if (id != other.id) return false - if (url != other.url) return false - if (inReplyToId != other.inReplyToId) return false - if (inReplyToAccountId != other.inReplyToAccountId) return false - if (account != other.account) return false - if (content.toString() != other.content.toString()) return false - if (createdAt != other.createdAt) return false - if (emojis != other.emojis) return false - if (favouritesCount != other.favouritesCount) return false - if (favourited != other.favourited) return false - if (sensitive != other.sensitive) return false - if (spoilerText != other.spoilerText) return false - if (attachments != other.attachments) return false - if (mentions != other.mentions) return false - if (tags != other.tags) return false - if (showingHiddenContent != other.showingHiddenContent) return false - if (expanded != other.expanded) return false - if (collapsible != other.collapsible) return false - if (collapsed != other.collapsed) return false - if (muted != other.muted) return false - if (poll != other.poll) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + (url?.hashCode() ?: 0) - result = 31 * result + (inReplyToId?.hashCode() ?: 0) - result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0) - result = 31 * result + account.hashCode() - result = 31 * result + content.toString().hashCode() - result = 31 * result + createdAt.hashCode() - result = 31 * result + emojis.hashCode() - result = 31 * result + favouritesCount - result = 31 * result + favourited.hashCode() - result = 31 * result + sensitive.hashCode() - result = 31 * result + spoilerText.hashCode() - result = 31 * result + attachments.hashCode() - result = 31 * result + mentions.hashCode() - result = 31 * result + tags.hashCode() - result = 31 * result + showingHiddenContent.hashCode() - result = 31 * result + expanded.hashCode() - result = 31 * result + collapsible.hashCode() - result = 31 * result + collapsed.hashCode() - result = 31 * result + muted.hashCode() - result = 31 * result + poll.hashCode() - return result - } - - fun toStatus(): Status { - return Status( - id = id, - url = url, - account = account.toAccount(), - inReplyToId = inReplyToId, - inReplyToAccountId = inReplyToAccountId, - content = content, - reblog = null, - createdAt = createdAt, - emojis = emojis, - reblogsCount = 0, - favouritesCount = favouritesCount, - reblogged = false, - favourited = favourited, - bookmarked = bookmarked, - sensitive = sensitive, - spoilerText = spoilerText, - visibility = Status.Visibility.DIRECT, - attachments = attachments, - mentions = mentions, - tags = tags, - application = null, - pinned = false, - muted = muted, - poll = poll, - card = null + fun toViewData(): StatusViewData.Concrete { + return StatusViewData.Concrete( + status = Status( + id = id, + url = url, + account = account.toAccount(), + inReplyToId = inReplyToId, + inReplyToAccountId = inReplyToAccountId, + content = content, + reblog = null, + createdAt = createdAt, + emojis = emojis, + reblogsCount = 0, + favouritesCount = favouritesCount, + reblogged = false, + favourited = favourited, + bookmarked = bookmarked, + sensitive = sensitive, + spoilerText = spoilerText, + visibility = Status.Visibility.DIRECT, + attachments = attachments, + mentions = mentions, + tags = tags, + application = null, + pinned = false, + muted = muted, + poll = poll, + card = null + ), + isExpanded = expanded, + isShowingContent = showingHiddenContent, + isCollapsed = collapsed ) } } @@ -202,7 +158,6 @@ fun Status.toEntity() = tags = tags, showingHiddenContent = false, expanded = false, - collapsible = shouldTrimStatus(content), collapsed = true, muted = muted ?: false, poll = poll diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt new file mode 100644 index 000000000..470675d17 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt @@ -0,0 +1,87 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.components.conversation + +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.viewdata.StatusViewData + +data class ConversationViewData( + val id: String, + val accounts: List, + val unread: Boolean, + val lastStatus: StatusViewData.Concrete +) { + fun toEntity( + accountId: Long, + favourited: Boolean = lastStatus.status.favourited, + bookmarked: Boolean = lastStatus.status.bookmarked, + muted: Boolean = lastStatus.status.muted ?: false, + poll: Poll? = lastStatus.status.poll, + expanded: Boolean = lastStatus.isExpanded, + collapsed: Boolean = lastStatus.isCollapsed, + showingHiddenContent: Boolean = lastStatus.isShowingContent + ): ConversationEntity { + return ConversationEntity( + accountId = accountId, + id = id, + accounts = accounts, + unread = unread, + lastStatus = lastStatus.toConversationStatusEntity( + favourited = favourited, + bookmarked = bookmarked, + muted = muted, + poll = poll, + expanded = expanded, + collapsed = collapsed, + showingHiddenContent = showingHiddenContent + ) + ) + } +} + +fun StatusViewData.Concrete.toConversationStatusEntity( + favourited: Boolean = status.favourited, + bookmarked: Boolean = status.bookmarked, + muted: Boolean = status.muted ?: false, + poll: Poll? = status.poll, + expanded: Boolean = isExpanded, + collapsed: Boolean = isCollapsed, + showingHiddenContent: Boolean = isShowingContent +): ConversationStatusEntity { + return ConversationStatusEntity( + id = id, + url = status.url, + inReplyToId = status.inReplyToId, + inReplyToAccountId = status.inReplyToAccountId, + account = status.account.toEntity(), + content = status.content, + createdAt = status.createdAt, + emojis = status.emojis, + favouritesCount = status.favouritesCount, + favourited = favourited, + bookmarked = bookmarked, + sensitive = status.sensitive, + spoilerText = status.spoilerText, + attachments = status.attachments, + mentions = status.mentions, + tags = status.tags, + showingHiddenContent = showingHiddenContent, + expanded = expanded, + collapsed = collapsed, + muted = muted, + poll = poll + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java index 436ba84ea..ffb88a942 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java @@ -28,11 +28,14 @@ import androidx.recyclerview.widget.RecyclerView; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.adapter.StatusBaseViewHolder; import com.keylesspalace.tusky.entity.Attachment; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.entity.TimelineAccount; import com.keylesspalace.tusky.interfaces.StatusActionListener; import com.keylesspalace.tusky.util.ImageLoadingHelper; import com.keylesspalace.tusky.util.SmartLengthInputFilter; import com.keylesspalace.tusky.util.StatusDisplayOptions; import com.keylesspalace.tusky.viewdata.PollViewDataKt; +import com.keylesspalace.tusky.viewdata.StatusViewData; import java.util.List; @@ -69,11 +72,12 @@ public class ConversationViewHolder extends StatusBaseViewHolder { return context.getResources().getDimensionPixelSize(R.dimen.status_media_preview_height); } - void setupWithConversation(ConversationEntity conversation) { - ConversationStatusEntity status = conversation.getLastStatus(); - ConversationAccountEntity account = status.getAccount(); + void setupWithConversation(ConversationViewData conversation) { + StatusViewData.Concrete statusViewData = conversation.getLastStatus(); + Status status = statusViewData.getStatus(); + TimelineAccount account = status.getAccount(); - setupCollapsedState(status.getCollapsible(), status.getCollapsed(), status.getExpanded(), status.getSpoilerText(), listener); + setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), statusViewData.getSpoilerText(), listener); setDisplayName(account.getDisplayName(), account.getEmojis(), statusDisplayOptions); setUsername(account.getUsername()); @@ -84,7 +88,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder { List attachments = status.getAttachments(); boolean sensitive = status.getSensitive(); if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { - setMediaPreviews(attachments, sensitive, listener, status.getShowingHiddenContent(), + setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(), statusDisplayOptions.useBlurhash()); if (attachments.size() == 0) { @@ -95,7 +99,7 @@ public class ConversationViewHolder extends StatusBaseViewHolder { mediaLabel.setVisibility(View.GONE); } } else { - setMediaLabel(attachments, sensitive, listener, status.getShowingHiddenContent()); + setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent()); // Hide all unused views. mediaPreviews[0].setVisibility(View.GONE); mediaPreviews[1].setVisibility(View.GONE); @@ -104,10 +108,10 @@ public class ConversationViewHolder extends StatusBaseViewHolder { hideSensitiveMediaWarning(); } - setupButtons(listener, account.getId(), status.getContent().toString(), + setupButtons(listener, account.getId(), statusViewData.getContent().toString(), statusDisplayOptions); - setSpoilerAndContent(status.getExpanded(), status.getContent(), status.getSpoilerText(), + setSpoilerAndContent(statusViewData.isExpanded(), statusViewData.getContent(), status.getSpoilerText(), status.getMentions(), status.getTags(), status.getEmojis(), PollViewDataKt.toViewData(status.getPoll()), statusDisplayOptions, listener); diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt index a09026c24..243c37448 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -153,24 +153,24 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onFavourite(favourite: Boolean, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.favourite(favourite, conversation) } } override fun onBookmark(favourite: Boolean, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.bookmark(favourite, conversation) } } override fun onMore(view: View, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> val popup = PopupMenu(requireContext(), view) popup.inflate(R.menu.conversation_more) - if (conversation.lastStatus.muted) { + if (conversation.lastStatus.status.muted == true) { popup.menu.removeItem(R.id.status_mute_conversation) } else { popup.menu.removeItem(R.id.status_unmute_conversation) @@ -189,14 +189,14 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { - adapter.item(position)?.let { conversation -> - viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.toStatus()), view) + adapter.peek(position)?.let { conversation -> + viewMedia(attachmentIndex, AttachmentViewData.list(conversation.lastStatus.status), view) } } override fun onViewThread(position: Int) { - adapter.item(position)?.let { conversation -> - viewThread(conversation.lastStatus.id, conversation.lastStatus.url) + adapter.peek(position)?.let { conversation -> + viewThread(conversation.lastStatus.id, conversation.lastStatus.status.url) } } @@ -205,13 +205,13 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onExpandedChange(expanded: Boolean, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.expandHiddenStatus(expanded, conversation) } } override fun onContentHiddenChange(isShowing: Boolean, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.showContent(isShowing, conversation) } } @@ -221,7 +221,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.collapseLongStatus(isCollapsed, conversation) } } @@ -241,12 +241,12 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onReply(position: Int) { - adapter.item(position)?.let { conversation -> - reply(conversation.lastStatus.toStatus()) + adapter.peek(position)?.let { conversation -> + reply(conversation.lastStatus.status) } } - private fun deleteConversation(conversation: ConversationEntity) { + private fun deleteConversation(conversation: ConversationViewData) { AlertDialog.Builder(requireContext()) .setMessage(R.string.dialog_delete_conversation_warning) .setNegativeButton(android.R.string.cancel, null) @@ -268,7 +268,7 @@ class ConversationsFragment : SFragment(), StatusActionListener, Injectable, Res } override fun onVoteInPoll(position: Int, choices: MutableList) { - adapter.item(position)?.let { conversation -> + adapter.peek(position)?.let { conversation -> viewModel.voteInPoll(choices, conversation) } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt index 396f8e486..9326a05c0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt @@ -16,16 +16,18 @@ package com.keylesspalace.tusky.components.conversation import android.util.Log +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.ExperimentalPagingApi import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn +import androidx.paging.map import com.keylesspalace.tusky.db.AccountManager import com.keylesspalace.tusky.db.AppDatabase import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases -import com.keylesspalace.tusky.util.RxAwareViewModel +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await import javax.inject.Inject @@ -35,7 +37,7 @@ class ConversationsViewModel @Inject constructor( private val database: AppDatabase, private val accountManager: AccountManager, private val api: MastodonApi -) : RxAwareViewModel() { +) : ViewModel() { @OptIn(ExperimentalPagingApi::class) val conversationFlow = Pager( @@ -44,104 +46,117 @@ class ConversationsViewModel @Inject constructor( pagingSourceFactory = { database.conversationDao().conversationsForAccount(accountManager.activeAccount!!.id) } ) .flow + .map { pagingData -> + pagingData.map { conversation -> conversation.toViewData() } + } .cachedIn(viewModelScope) - fun favourite(favourite: Boolean, conversation: ConversationEntity) { + fun favourite(favourite: Boolean, conversation: ConversationViewData) { viewModelScope.launch { try { timelineCases.favourite(conversation.lastStatus.id, favourite).await() - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(favourited = favourite) + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + favourited = favourite ) - database.conversationDao().insert(newConversation) + saveConversationToDb(newConversation) } catch (e: Exception) { Log.w(TAG, "failed to favourite status", e) } } } - fun bookmark(bookmark: Boolean, conversation: ConversationEntity) { + fun bookmark(bookmark: Boolean, conversation: ConversationViewData) { viewModelScope.launch { try { timelineCases.bookmark(conversation.lastStatus.id, bookmark).await() - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(bookmarked = bookmark) + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + bookmarked = bookmark ) - database.conversationDao().insert(newConversation) + saveConversationToDb(newConversation) } catch (e: Exception) { Log.w(TAG, "failed to bookmark status", e) } } } - fun voteInPoll(choices: List, conversation: ConversationEntity) { + fun voteInPoll(choices: List, conversation: ConversationViewData) { viewModelScope.launch { try { - val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.poll?.id!!, choices).await() - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(poll = poll) + val poll = timelineCases.voteInPoll(conversation.lastStatus.id, conversation.lastStatus.status.poll?.id!!, choices).await() + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + poll = poll ) - database.conversationDao().insert(newConversation) + saveConversationToDb(newConversation) } catch (e: Exception) { Log.w(TAG, "failed to vote in poll", e) } } } - fun expandHiddenStatus(expanded: Boolean, conversation: ConversationEntity) { + fun expandHiddenStatus(expanded: Boolean, conversation: ConversationViewData) { viewModelScope.launch { - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(expanded = expanded) + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + expanded = expanded ) saveConversationToDb(newConversation) } } - fun collapseLongStatus(collapsed: Boolean, conversation: ConversationEntity) { + fun collapseLongStatus(collapsed: Boolean, conversation: ConversationViewData) { viewModelScope.launch { - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(collapsed = collapsed) + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + collapsed = collapsed ) saveConversationToDb(newConversation) } } - fun showContent(showing: Boolean, conversation: ConversationEntity) { + fun showContent(showing: Boolean, conversation: ConversationViewData) { viewModelScope.launch { - val newConversation = conversation.copy( - lastStatus = conversation.lastStatus.copy(showingHiddenContent = showing) + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + showingHiddenContent = showing ) saveConversationToDb(newConversation) } } - fun remove(conversation: ConversationEntity) { + fun remove(conversation: ConversationViewData) { viewModelScope.launch { try { api.deleteConversation(conversationId = conversation.id) - database.conversationDao().delete(conversation) + database.conversationDao().delete( + id = conversation.id, + accountId = accountManager.activeAccount!!.id + ) } catch (e: Exception) { Log.w(TAG, "failed to delete conversation", e) } } } - fun muteConversation(conversation: ConversationEntity) { + fun muteConversation(conversation: ConversationViewData) { viewModelScope.launch { try { - val newStatus = timelineCases.muteConversation( + timelineCases.muteConversation( conversation.lastStatus.id, - !conversation.lastStatus.muted + !(conversation.lastStatus.status.muted ?: false) ).await() - val newConversation = conversation.copy( - lastStatus = newStatus.toEntity() + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + muted = !(conversation.lastStatus.status.muted ?: false) ) database.conversationDao().insert(newConversation) @@ -151,7 +166,7 @@ class ConversationsViewModel @Inject constructor( } } - suspend fun saveConversationToDb(conversation: ConversationEntity) { + private suspend fun saveConversationToDb(conversation: ConversationEntity) { database.conversationDao().insert(conversation) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt index f8991282d..9f99da530 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.viewModelScope import androidx.paging.Pager import androidx.paging.PagingConfig import androidx.paging.cachedIn +import androidx.paging.map import com.keylesspalace.tusky.appstore.BlockEvent import com.keylesspalace.tusky.appstore.EventHub import com.keylesspalace.tusky.appstore.MuteEvent @@ -34,11 +35,13 @@ import com.keylesspalace.tusky.util.Loading import com.keylesspalace.tusky.util.Resource import com.keylesspalace.tusky.util.RxAwareViewModel import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.toViewData import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import javax.inject.Inject @@ -74,6 +77,11 @@ class ReportViewModel @Inject constructor( pagingSourceFactory = { StatusesPagingSource(accountId, mastodonApi) } ).flow } + .map { pagingData -> + /* TODO: refactor reports to use the isShowingContent / isExpanded / isCollapsed attributes from StatusViewData.Concrete + instead of StatusViewState */ + pagingData.map { status -> status.toViewData(false, false, false) } + } .cachedIn(viewModelScope) private val selectedIds = HashSet() @@ -155,7 +163,7 @@ class ReportViewModel @Inject constructor( .observeOn(AndroidSchedulers.mainThread()) .subscribe( { relationship -> - val muting = relationship?.muting == true + val muting = relationship.muting muteStateMutable.value = Success(muting) if (muting) { eventHub.dispatch(MuteEvent(accountId)) @@ -180,7 +188,7 @@ class ReportViewModel @Inject constructor( .observeOn(AndroidSchedulers.mainThread()) .subscribe( { relationship -> - val blocking = relationship?.blocking == true + val blocking = relationship.blocking blockStateMutable.value = Success(blocking) if (blocking) { eventHub.dispatch(BlockEvent(accountId)) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt index 1b3b0de62..9dceddecb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt @@ -37,6 +37,7 @@ import com.keylesspalace.tusky.util.setClickableMentions import com.keylesspalace.tusky.util.setClickableText import com.keylesspalace.tusky.util.shouldTrimStatus import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.viewdata.StatusViewData import com.keylesspalace.tusky.viewdata.toViewData import java.util.Date @@ -45,20 +46,21 @@ class StatusViewHolder( private val statusDisplayOptions: StatusDisplayOptions, private val viewState: StatusViewState, private val adapterHandler: AdapterHandler, - private val getStatusForPosition: (Int) -> Status? + private val getStatusForPosition: (Int) -> StatusViewData.Concrete? ) : RecyclerView.ViewHolder(binding.root) { + private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize(R.dimen.status_media_preview_height) private val statusViewHelper = StatusViewHelper(itemView) private val previewListener = object : StatusViewHelper.MediaPreviewListener { override fun onViewMedia(v: View?, idx: Int) { - status()?.let { status -> - adapterHandler.showMedia(v, status, idx) + viewdata()?.let { viewdata -> + adapterHandler.showMedia(v, viewdata.status, idx) } } override fun onContentHiddenChange(isShowing: Boolean) { - status()?.id?.let { id -> + viewdata()?.id?.let { id -> viewState.setMediaShow(id, isShowing) } } @@ -66,57 +68,57 @@ class StatusViewHolder( init { binding.statusSelection.setOnCheckedChangeListener { _, isChecked -> - status()?.let { status -> - adapterHandler.setStatusChecked(status, isChecked) + viewdata()?.let { viewdata -> + adapterHandler.setStatusChecked(viewdata.status, isChecked) } } binding.statusMediaPreviewContainer.clipToOutline = true } - fun bind(status: Status) { - binding.statusSelection.isChecked = adapterHandler.isStatusChecked(status.id) + fun bind(viewData: StatusViewData.Concrete) { + binding.statusSelection.isChecked = adapterHandler.isStatusChecked(viewData.id) updateTextView() - val sensitive = status.sensitive + val sensitive = viewData.status.sensitive statusViewHelper.setMediasPreview( - statusDisplayOptions, status.attachments, - sensitive, previewListener, viewState.isMediaShow(status.id, status.sensitive), + statusDisplayOptions, viewData.status.attachments, + sensitive, previewListener, viewState.isMediaShow(viewData.id, viewData.status.sensitive), mediaViewHeight ) - statusViewHelper.setupPollReadonly(status.poll.toViewData(), status.emojis, statusDisplayOptions) - setCreatedAt(status.createdAt) + statusViewHelper.setupPollReadonly(viewData.status.poll.toViewData(), viewData.status.emojis, statusDisplayOptions) + setCreatedAt(viewData.status.createdAt) } private fun updateTextView() { - status()?.let { status -> + viewdata()?.let { viewdata -> setupCollapsedState( - shouldTrimStatus(status.content), viewState.isCollapsed(status.id, true), - viewState.isContentShow(status.id, status.sensitive), status.spoilerText + shouldTrimStatus(viewdata.content), viewState.isCollapsed(viewdata.id, true), + viewState.isContentShow(viewdata.id, viewdata.status.sensitive), viewdata.spoilerText ) - if (status.spoilerText.isBlank()) { - setTextVisible(true, status.content, status.mentions, status.tags, status.emojis, adapterHandler) + if (viewdata.spoilerText.isBlank()) { + setTextVisible(true, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler) binding.statusContentWarningButton.hide() binding.statusContentWarningDescription.hide() } else { - val emojiSpoiler = status.spoilerText.emojify(status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis) + val emojiSpoiler = viewdata.spoilerText.emojify(viewdata.status.emojis, binding.statusContentWarningDescription, statusDisplayOptions.animateEmojis) binding.statusContentWarningDescription.text = emojiSpoiler binding.statusContentWarningDescription.show() binding.statusContentWarningButton.show() - setContentWarningButtonText(viewState.isContentShow(status.id, true)) + setContentWarningButtonText(viewState.isContentShow(viewdata.id, true)) binding.statusContentWarningButton.setOnClickListener { - status()?.let { status -> - val contentShown = viewState.isContentShow(status.id, true) + viewdata()?.let { viewdata -> + val contentShown = viewState.isContentShow(viewdata.id, true) binding.statusContentWarningDescription.invalidate() - viewState.setContentShow(status.id, !contentShown) - setTextVisible(!contentShown, status.content, status.mentions, status.tags, status.emojis, adapterHandler) + viewState.setContentShow(viewdata.id, !contentShown) + setTextVisible(!contentShown, viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler) setContentWarningButtonText(!contentShown) } } - setTextVisible(viewState.isContentShow(status.id, true), status.content, status.mentions, status.tags, status.emojis, adapterHandler) + setTextVisible(viewState.isContentShow(viewdata.id, true), viewdata.content, viewdata.status.mentions, viewdata.status.tags, viewdata.status.emojis, adapterHandler) } } } @@ -169,8 +171,8 @@ class StatusViewHolder( /* input filter for TextViews have to be set before text */ if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { binding.buttonToggleContent.setOnClickListener { - status()?.let { status -> - viewState.setCollapsed(status.id, !collapsed) + viewdata()?.let { viewdata -> + viewState.setCollapsed(viewdata.id, !collapsed) updateTextView() } } @@ -189,5 +191,5 @@ class StatusViewHolder( } } - private fun status() = getStatusForPosition(bindingAdapterPosition) + private fun viewdata() = getStatusForPosition(bindingAdapterPosition) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt index 76ed2ebea..314513eb9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt @@ -22,16 +22,16 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.keylesspalace.tusky.components.report.model.StatusViewState import com.keylesspalace.tusky.databinding.ItemReportStatusBinding -import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.StatusViewData class StatusesAdapter( private val statusDisplayOptions: StatusDisplayOptions, private val statusViewState: StatusViewState, private val adapterHandler: AdapterHandler -) : PagingDataAdapter(STATUS_COMPARATOR) { +) : PagingDataAdapter(STATUS_COMPARATOR) { - private val statusForPosition: (Int) -> Status? = { position: Int -> + private val statusForPosition: (Int) -> StatusViewData.Concrete? = { position: Int -> if (position != RecyclerView.NO_POSITION) getItem(position) else null } @@ -50,11 +50,11 @@ class StatusesAdapter( } companion object { - val STATUS_COMPARATOR = object : DiffUtil.ItemCallback() { - override fun areContentsTheSame(oldItem: Status, newItem: Status): Boolean = + val STATUS_COMPARATOR = object : DiffUtil.ItemCallback() { + override fun areContentsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean = oldItem == newItem - override fun areItemsTheSame(oldItem: Status, newItem: Status): Boolean = + override fun areItemsTheSame(oldItem: StatusViewData.Concrete, newItem: StatusViewData.Concrete): Boolean = oldItem.id == newItem.id } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt index 252b98800..6ec954239 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt @@ -15,9 +15,6 @@ package com.keylesspalace.tusky.components.timeline -import android.text.SpannedString -import androidx.core.text.parseAsHtml -import androidx.core.text.toHtml import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.keylesspalace.tusky.db.TimelineAccountEntity @@ -29,8 +26,6 @@ import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.TimelineAccount -import com.keylesspalace.tusky.util.shouldTrimStatus -import com.keylesspalace.tusky.util.trimTrailingWhitespace import com.keylesspalace.tusky.viewdata.StatusViewData import java.util.Date @@ -119,7 +114,7 @@ fun Status.toEntity( authorServerId = actionableStatus.account.id, inReplyToId = actionableStatus.inReplyToId, inReplyToAccountId = actionableStatus.inReplyToAccountId, - content = actionableStatus.content.toHtml(), + content = actionableStatus.content, createdAt = actionableStatus.createdAt.time, emojis = actionableStatus.emojis.let(gson::toJson), reblogsCount = actionableStatus.reblogsCount, @@ -165,8 +160,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { inReplyToId = status.inReplyToId, inReplyToAccountId = status.inReplyToAccountId, reblog = null, - content = status.content?.parseAsHtml()?.trimTrailingWhitespace() - ?: SpannedString(""), + content = status.content.orEmpty(), createdAt = Date(status.createdAt), emojis = emojis, reblogsCount = status.reblogsCount, @@ -195,7 +189,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { inReplyToId = null, inReplyToAccountId = null, reblog = reblog, - content = SpannedString(""), + content = "", createdAt = Date(status.createdAt), // lie but whatever? emojis = listOf(), reblogsCount = 0, @@ -223,8 +217,7 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { inReplyToId = status.inReplyToId, inReplyToAccountId = status.inReplyToAccountId, reblog = null, - content = status.content?.parseAsHtml()?.trimTrailingWhitespace() - ?: SpannedString(""), + content = status.content.orEmpty(), createdAt = Date(status.createdAt), emojis = emojis, reblogsCount = status.reblogsCount, @@ -249,7 +242,6 @@ fun TimelineStatusWithAccount.toViewData(gson: Gson): StatusViewData { status = status, isExpanded = this.status.expanded, isShowingContent = this.status.contentShowing, - isCollapsible = shouldTrimStatus(status.content), isCollapsed = this.status.contentCollapsed ) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt index 304b4e5a0..7158a7b3a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt @@ -42,7 +42,10 @@ import com.keylesspalace.tusky.network.FilterModel import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.network.TimelineCases import com.keylesspalace.tusky.viewdata.StatusViewData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await @@ -79,15 +82,13 @@ class CachedTimelineViewModel @Inject constructor( } ).flow .map { pagingData -> - pagingData.map { timelineStatus -> + pagingData.map(Dispatchers.Default.asExecutor()) { timelineStatus -> timelineStatus.toViewData(gson) - } - } - .map { pagingData -> - pagingData.filter { statusViewData -> + }.filter(Dispatchers.Default.asExecutor()) { statusViewData -> !shouldFilterStatus(statusViewData) } } + .flowOn(Dispatchers.Default) .cachedIn(viewModelScope) init { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt index f70fdcc81..ca7988bb9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt @@ -40,6 +40,9 @@ import com.keylesspalace.tusky.util.isLessThan import com.keylesspalace.tusky.util.isLessThanOrEqual import com.keylesspalace.tusky.util.toViewData import com.keylesspalace.tusky.viewdata.StatusViewData +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.rx3.await @@ -79,10 +82,11 @@ class NetworkTimelineViewModel @Inject constructor( remoteMediator = NetworkTimelineRemoteMediator(accountManager, this) ).flow .map { pagingData -> - pagingData.filter { statusViewData -> + pagingData.filter(Dispatchers.Default.asExecutor()) { statusViewData -> !shouldFilterStatus(statusViewData) } } + .flowOn(Dispatchers.Default) .cachedIn(viewModelScope) override fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) { diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index 2131300c7..c541958ae 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -31,7 +31,7 @@ import java.io.File; */ @Database(entities = { DraftEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, TimelineAccountEntity.class, ConversationEntity.class - }, version = 32) + }, version = 33) public abstract class AppDatabase extends RoomDatabase { public abstract AccountDao accountDao(); @@ -490,4 +490,41 @@ public abstract class AppDatabase extends RoomDatabase { database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSignUps` INTEGER NOT NULL DEFAULT 1"); } }; + + public static final Migration MIGRATION_32_33 = new Migration(32, 33) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + + // ConversationEntity lost the s_collapsible column + // since SQLite does not support removing columns and it is just a cache table, we recreate the whole table. + database.execSQL("DROP TABLE `ConversationEntity`"); + database.execSQL("CREATE TABLE IF NOT EXISTS `ConversationEntity` (" + + "`accountId` INTEGER NOT NULL," + + "`id` TEXT NOT NULL," + + "`accounts` TEXT NOT NULL," + + "`unread` INTEGER NOT NULL," + + "`s_id` TEXT NOT NULL," + + "`s_url` TEXT," + + "`s_inReplyToId` TEXT," + + "`s_inReplyToAccountId` TEXT," + + "`s_account` TEXT NOT NULL," + + "`s_content` TEXT NOT NULL," + + "`s_createdAt` INTEGER NOT NULL," + + "`s_emojis` TEXT NOT NULL," + + "`s_favouritesCount` INTEGER NOT NULL," + + "`s_favourited` INTEGER NOT NULL," + + "`s_bookmarked` INTEGER NOT NULL," + + "`s_sensitive` INTEGER NOT NULL," + + "`s_spoilerText` TEXT NOT NULL," + + "`s_attachments` TEXT NOT NULL," + + "`s_mentions` TEXT NOT NULL," + + "`s_tags` TEXT," + + "`s_showingHiddenContent` INTEGER NOT NULL," + + "`s_expanded` INTEGER NOT NULL," + + "`s_collapsed` INTEGER NOT NULL," + + "`s_muted` INTEGER NOT NULL," + + "`s_poll` TEXT," + + "PRIMARY KEY(`id`, `accountId`))"); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt index 393a23925..fe093bd0c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt @@ -17,7 +17,6 @@ package com.keylesspalace.tusky.db import androidx.paging.PagingSource import androidx.room.Dao -import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query @@ -31,8 +30,8 @@ interface ConversationsDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(conversation: ConversationEntity): Long - @Delete - suspend fun delete(conversation: ConversationEntity): Int + @Query("DELETE FROM ConversationEntity WHERE id = :id AND accountId = :accountId") + suspend fun delete(id: String, accountId: Long): Int @Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY s_createdAt DESC") fun conversationsForAccount(accountId: Long): PagingSource diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt index c9daec0a1..34ff6474b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -15,9 +15,6 @@ package com.keylesspalace.tusky.db -import android.text.Spanned -import androidx.core.text.parseAsHtml -import androidx.core.text.toHtml import androidx.room.ProvidedTypeConverter import androidx.room.TypeConverter import com.google.gson.Gson @@ -31,10 +28,8 @@ import com.keylesspalace.tusky.entity.HashTag import com.keylesspalace.tusky.entity.NewPoll import com.keylesspalace.tusky.entity.Poll import com.keylesspalace.tusky.entity.Status -import com.keylesspalace.tusky.util.trimTrailingWhitespace import java.net.URLDecoder import java.net.URLEncoder -import java.util.ArrayList import java.util.Date import javax.inject.Inject import javax.inject.Singleton @@ -140,22 +135,6 @@ class Converters @Inject constructor ( return Date(date) } - @TypeConverter - fun spannedToString(spanned: Spanned?): String? { - if (spanned == null) { - return null - } - return spanned.toHtml() - } - - @TypeConverter - fun stringToSpanned(spannedString: String?): Spanned? { - if (spannedString == null) { - return null - } - return spannedString.parseAsHtml().trimTrailingWhitespace() - } - @TypeConverter fun pollToJson(poll: Poll?): String? { return gson.toJson(poll) diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index 677f81677..7f0fbd015 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -63,6 +63,7 @@ class AppModule { AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")), AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29, AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32, + AppDatabase.MIGRATION_32_33 ) .build() } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index d927c2999..d8b52ca38 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -18,13 +18,10 @@ package com.keylesspalace.tusky.di import android.content.Context import android.content.SharedPreferences import android.os.Build -import android.text.Spanned import at.connyduck.calladapter.kotlinresult.KotlinResultCallAdapterFactory import com.google.gson.Gson -import com.google.gson.GsonBuilder import com.keylesspalace.tusky.BuildConfig import com.keylesspalace.tusky.db.AccountManager -import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor import com.keylesspalace.tusky.network.MastodonApi import com.keylesspalace.tusky.util.getNonNullString @@ -52,11 +49,7 @@ class NetworkModule { @Provides @Singleton - fun providesGson(): Gson { - return GsonBuilder() - .registerTypeAdapter(Spanned::class.java, SpannedTypeAdapter()) - .create() - } + fun providesGson() = Gson() @Provides @Singleton diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt index 672bd5aae..bf5431ee6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt @@ -15,7 +15,6 @@ package com.keylesspalace.tusky.entity -import android.text.Spanned import com.google.gson.annotations.SerializedName import java.util.Date @@ -24,7 +23,7 @@ data class Account( @SerializedName("username") val localUsername: String, @SerializedName("acct") val username: String, @SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract - val note: Spanned, + val note: String, val url: String, val avatar: String, val header: String, @@ -46,56 +45,6 @@ data class Account( } else displayName fun isRemote(): Boolean = this.username != this.localUsername - - /** - * overriding equals & hashcode because Spanned does not always compare correctly otherwise - */ - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as Account - - if (id != other.id) return false - if (localUsername != other.localUsername) return false - if (username != other.username) return false - if (displayName != other.displayName) return false - if (note.toString() != other.note.toString()) return false - if (url != other.url) return false - if (avatar != other.avatar) return false - if (header != other.header) return false - if (locked != other.locked) return false - if (followersCount != other.followersCount) return false - if (followingCount != other.followingCount) return false - if (statusesCount != other.statusesCount) return false - if (source != other.source) return false - if (bot != other.bot) return false - if (emojis != other.emojis) return false - if (fields != other.fields) return false - if (moved != other.moved) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + localUsername.hashCode() - result = 31 * result + username.hashCode() - result = 31 * result + (displayName?.hashCode() ?: 0) - result = 31 * result + note.toString().hashCode() - result = 31 * result + url.hashCode() - result = 31 * result + avatar.hashCode() - result = 31 * result + header.hashCode() - result = 31 * result + locked.hashCode() - result = 31 * result + followersCount - result = 31 * result + followingCount - result = 31 * result + statusesCount - result = 31 * result + (source?.hashCode() ?: 0) - result = 31 * result + bot.hashCode() - result = 31 * result + (emojis?.hashCode() ?: 0) - result = 31 * result + (fields?.hashCode() ?: 0) - result = 31 * result + (moved?.hashCode() ?: 0) - return result - } } data class AccountSource( @@ -107,7 +56,7 @@ data class AccountSource( data class Field( val name: String, - val value: Spanned, + val value: String, @SerializedName("verified_at") val verifiedAt: Date? ) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt index 400e9764d..00d5659d5 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt @@ -15,13 +15,12 @@ package com.keylesspalace.tusky.entity -import android.text.Spanned import com.google.gson.annotations.SerializedName import java.util.Date data class Announcement( val id: String, - val content: Spanned, + val content: String, @SerializedName("starts_at") val startsAt: Date?, @SerializedName("ends_at") val endsAt: Date?, @SerializedName("all_day") val allDay: Boolean, diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt index 52011f3d1..29fe7f8ee 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt @@ -15,13 +15,12 @@ package com.keylesspalace.tusky.entity -import android.text.Spanned import com.google.gson.annotations.SerializedName data class Card( val url: String, - val title: Spanned, - val description: Spanned, + val title: String, + val description: String, @SerializedName("author_name") val authorName: String, val image: String, val type: String, @@ -31,9 +30,7 @@ data class Card( val embed_url: String? ) { - override fun hashCode(): Int { - return url.hashCode() - } + override fun hashCode() = url.hashCode() override fun equals(other: Any?): Boolean { if (other !is Card) { diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt index f75ce4e76..19cb7aa64 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -16,9 +16,9 @@ package com.keylesspalace.tusky.entity import android.text.SpannableStringBuilder -import android.text.Spanned import android.text.style.URLSpan import com.google.gson.annotations.SerializedName +import com.keylesspalace.tusky.util.parseAsMastodonHtml import java.util.ArrayList import java.util.Date @@ -29,7 +29,7 @@ data class Status( @SerializedName("in_reply_to_id") var inReplyToId: String?, @SerializedName("in_reply_to_account_id") val inReplyToAccountId: String?, val reblog: Status?, - val content: Spanned, + val content: String, @SerializedName("created_at") val createdAt: Date, val emojis: List, @SerializedName("reblogs_count") val reblogsCount: Int, @@ -134,8 +134,9 @@ data class Status( } private fun getEditableText(): String { - val builder = SpannableStringBuilder(content) - for (span in content.getSpans(0, content.length, URLSpan::class.java)) { + val contentSpanned = content.parseAsMastodonHtml() + val builder = SpannableStringBuilder(content.parseAsMastodonHtml()) + for (span in contentSpanned.getSpans(0, content.length, URLSpan::class.java)) { val url = span.url for ((_, url1, username) in mentions) { if (url == url1) { @@ -149,71 +150,6 @@ data class Status( return builder.toString() } - /** - * overriding equals & hashcode because Spanned does not always compare correctly otherwise - */ - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as Status - - if (id != other.id) return false - if (url != other.url) return false - if (account != other.account) return false - if (inReplyToId != other.inReplyToId) return false - if (inReplyToAccountId != other.inReplyToAccountId) return false - if (reblog != other.reblog) return false - if (content.toString() != other.content.toString()) return false - if (createdAt != other.createdAt) return false - if (emojis != other.emojis) return false - if (reblogsCount != other.reblogsCount) return false - if (favouritesCount != other.favouritesCount) return false - if (reblogged != other.reblogged) return false - if (favourited != other.favourited) return false - if (bookmarked != other.bookmarked) return false - if (sensitive != other.sensitive) return false - if (spoilerText != other.spoilerText) return false - if (visibility != other.visibility) return false - if (attachments != other.attachments) return false - if (mentions != other.mentions) return false - if (tags != other.tags) return false - if (application != other.application) return false - if (pinned != other.pinned) return false - if (muted != other.muted) return false - if (poll != other.poll) return false - if (card != other.card) return false - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + (url?.hashCode() ?: 0) - result = 31 * result + account.hashCode() - result = 31 * result + (inReplyToId?.hashCode() ?: 0) - result = 31 * result + (inReplyToAccountId?.hashCode() ?: 0) - result = 31 * result + (reblog?.hashCode() ?: 0) - result = 31 * result + content.toString().hashCode() - result = 31 * result + createdAt.hashCode() - result = 31 * result + emojis.hashCode() - result = 31 * result + reblogsCount - result = 31 * result + favouritesCount - result = 31 * result + reblogged.hashCode() - result = 31 * result + favourited.hashCode() - result = 31 * result + bookmarked.hashCode() - result = 31 * result + sensitive.hashCode() - result = 31 * result + spoilerText.hashCode() - result = 31 * result + visibility.hashCode() - result = 31 * result + attachments.hashCode() - result = 31 * result + mentions.hashCode() - result = 31 * result + (tags?.hashCode() ?: 0) - result = 31 * result + (application?.hashCode() ?: 0) - result = 31 * result + (pinned?.hashCode() ?: 0) - result = 31 * result + (muted?.hashCode() ?: 0) - result = 31 * result + (poll?.hashCode() ?: 0) - result = 31 * result + (card?.hashCode() ?: 0) - return result - } - data class Mention( val id: String, val url: String, diff --git a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt deleted file mode 100644 index 60af61342..000000000 --- a/app/src/main/java/com/keylesspalace/tusky/json/SpannedTypeAdapter.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* Copyright 2020 Tusky Contributors - * - * This file is a part of Tusky. - * - * This program is free software; you can redistribute it and/or modify it under the terms of the - * GNU General Public License as published by the Free Software Foundation; either version 3 of the - * License, or (at your option) any later version. - * - * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tusky; if not, - * see . */ - -package com.keylesspalace.tusky.json - -import android.text.Spanned -import android.text.SpannedString -import androidx.core.text.HtmlCompat -import androidx.core.text.parseAsHtml -import androidx.core.text.toHtml -import com.google.gson.JsonDeserializationContext -import com.google.gson.JsonDeserializer -import com.google.gson.JsonElement -import com.google.gson.JsonParseException -import com.google.gson.JsonPrimitive -import com.google.gson.JsonSerializationContext -import com.google.gson.JsonSerializer -import com.keylesspalace.tusky.util.trimTrailingWhitespace -import java.lang.reflect.Type - -class SpannedTypeAdapter : JsonDeserializer, JsonSerializer { - @Throws(JsonParseException::class) - override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Spanned { - return json.asString - /* Mastodon uses 'white-space: pre-wrap;' so spaces are displayed as returned by the Api. - * We can't use CSS so we replace spaces with non-breaking-spaces to emulate the behavior. - */ - ?.replace("
", "
 ") - ?.replace("
", "
 ") - ?.replace("
", "
 ") - ?.replace(" ", "  ") - ?.parseAsHtml() - /* Html.fromHtml returns trailing whitespace if the html ends in a

tag, which - * most status contents do, so it should be trimmed. */ - ?.trimTrailingWhitespace() - ?: SpannedString("") - } - - override fun serialize(src: Spanned?, typeOfSrc: Type, context: JsonSerializationContext): JsonElement { - return JsonPrimitive(src!!.toHtml(HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL)) - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt new file mode 100644 index 000000000..fc62c78d6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt @@ -0,0 +1,62 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see . */ + +package com.keylesspalace.tusky.util + +import android.text.SpannableStringBuilder +import android.text.Spanned +import androidx.core.text.parseAsHtml + +/** + * parse a String containing html from the Mastodon api to Spanned + */ +fun String.parseAsMastodonHtml(): Spanned { + return this.replace("
", "
 ") + .replace("
", "
 ") + .replace("
", "
 ") + .replace(" ", "  ") + .parseAsHtml() + /* Html.fromHtml returns trailing whitespace if the html ends in a

tag, which + * most status contents do, so it should be trimmed. */ + .trimTrailingWhitespace() +} + +fun replaceCrashingCharacters(content: Spanned): Spanned { + return replaceCrashingCharacters(content as CharSequence) as Spanned +} + +fun replaceCrashingCharacters(content: CharSequence): CharSequence? { + var replacing = false + var builder: SpannableStringBuilder? = null + val length = content.length + for (index in 0 until length) { + val character = content[index] + + // If there are more than one or two, switch to a map + if (character == SOFT_HYPHEN) { + if (!replacing) { + replacing = true + builder = SpannableStringBuilder(content, 0, index) + } + builder!!.append(ASCII_HYPHEN) + } else if (replacing) { + builder!!.append(character) + } + } + return if (replacing) builder else content +} + +private const val SOFT_HYPHEN = '\u00ad' +private const val ASCII_HYPHEN = '-' diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt index 52d9713f4..fef9c0bb8 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt @@ -27,12 +27,9 @@ fun Status.toViewData( isExpanded: Boolean, isCollapsed: Boolean ): StatusViewData.Concrete { - val visibleStatus = this.reblog ?: this - return StatusViewData.Concrete( status = this, isShowingContent = isShowingContent, - isCollapsible = shouldTrimStatus(visibleStatus.content), isCollapsed = isCollapsed, isExpanded = isExpanded, ) diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt index d8f271578..8ac212d90 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt @@ -15,9 +15,11 @@ package com.keylesspalace.tusky.viewdata import android.os.Build -import android.text.SpannableStringBuilder import android.text.Spanned import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.util.parseAsMastodonHtml +import com.keylesspalace.tusky.util.replaceCrashingCharacters +import com.keylesspalace.tusky.util.shouldTrimStatus /** * Created by charlag on 11/07/2017. @@ -32,13 +34,6 @@ sealed class StatusViewData { val status: Status, val isExpanded: Boolean, val isShowingContent: Boolean, - /** - * Specifies whether the content of this post is allowed to be collapsed or if it should show - * all content regardless. - * - * @return Whether the post is collapsible or never collapsed. - */ - val isCollapsible: Boolean, /** * Specifies whether the content of this post is currently limited in visibility to the first * 500 characters or not. @@ -51,6 +46,14 @@ sealed class StatusViewData { override val id: String get() = status.id + /** + * Specifies whether the content of this post is allowed to be collapsed or if it should show + * all content regardless. + * + * @return Whether the post is collapsible or never collapsed. + */ + val isCollapsible: Boolean + val content: Spanned val spoilerText: String val username: String @@ -74,45 +77,17 @@ sealed class StatusViewData { init { if (Build.VERSION.SDK_INT == 23) { // https://github.com/tuskyapp/Tusky/issues/563 - this.content = replaceCrashingCharacters(status.actionableStatus.content) + this.content = replaceCrashingCharacters(status.actionableStatus.content.parseAsMastodonHtml()) this.spoilerText = replaceCrashingCharacters(status.actionableStatus.spoilerText).toString() this.username = replaceCrashingCharacters(status.actionableStatus.account.username).toString() } else { - this.content = status.actionableStatus.content + this.content = status.actionableStatus.content.parseAsMastodonHtml() this.spoilerText = status.actionableStatus.spoilerText this.username = status.actionableStatus.account.username } - } - - companion object { - private const val SOFT_HYPHEN = '\u00ad' - private const val ASCII_HYPHEN = '-' - fun replaceCrashingCharacters(content: Spanned): Spanned { - return replaceCrashingCharacters(content as CharSequence) as Spanned - } - - fun replaceCrashingCharacters(content: CharSequence): CharSequence? { - var replacing = false - var builder: SpannableStringBuilder? = null - val length = content.length - for (index in 0 until length) { - val character = content[index] - - // If there are more than one or two, switch to a map - if (character == SOFT_HYPHEN) { - if (!replacing) { - replacing = true - builder = SpannableStringBuilder(content, 0, index) - } - builder!!.append(ASCII_HYPHEN) - } else if (replacing) { - builder!!.append(character) - } - } - return if (replacing) builder else content - } + this.isCollapsible = shouldTrimStatus(this.content) } /** Helper for Java */ diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt index ff2088231..beb6af9b4 100644 --- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt @@ -15,7 +15,6 @@ package com.keylesspalace.tusky -import android.text.SpannedString import androidx.arch.core.executor.testing.InstantTaskExecutorRule import com.keylesspalace.tusky.entity.SearchResult import com.keylesspalace.tusky.entity.Status @@ -70,7 +69,7 @@ class BottomSheetActivityTest { inReplyToId = null, inReplyToAccountId = null, reblog = null, - content = SpannedString("omgwat"), + content = "omgwat", createdAt = Date(), emojis = emptyList(), reblogsCount = 0, diff --git a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt index dc4a412ff..5396a21ec 100644 --- a/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt @@ -17,7 +17,6 @@ package com.keylesspalace.tusky import android.content.Intent import android.os.Looper.getMainLooper -import android.text.SpannedString import android.widget.EditText import androidx.test.ext.junit.runners.AndroidJUnit4 import com.keylesspalace.tusky.components.compose.ComposeActivity @@ -469,7 +468,7 @@ class ComposeActivityTest { "admin", "admin", "admin", - SpannedString(""), + "", "https://example.token", "", "", diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt index d50639431..91ea38d3b 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt @@ -1,6 +1,5 @@ package com.keylesspalace.tusky -import android.text.SpannedString import androidx.test.ext.junit.runners.AndroidJUnit4 import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Filter @@ -162,7 +161,7 @@ class FilterTest { inReplyToId = null, inReplyToAccountId = null, reblog = null, - content = SpannedString(content), + content = content, createdAt = Date(), emojis = emptyList(), reblogsCount = 0, diff --git a/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt b/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt index ed06e27c6..3086036a0 100644 --- a/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt @@ -1,10 +1,8 @@ package com.keylesspalace.tusky -import android.text.Spanned import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.gson.GsonBuilder +import com.google.gson.Gson import com.keylesspalace.tusky.entity.Status -import com.keylesspalace.tusky.json.SpannedTypeAdapter import com.keylesspalace.tusky.viewdata.StatusViewData import org.junit.Assert.assertEquals import org.junit.Assert.assertNotEquals @@ -39,9 +37,7 @@ class StatusComparisonTest { assertEquals(createStatus(note = "Test"), createStatus(note = "Test 123456")) } - private val gson = GsonBuilder().registerTypeAdapter( - Spanned::class.java, SpannedTypeAdapter() - ).create() + private val gson = Gson() @Test fun `two equal status view data - should be equal`() { @@ -49,14 +45,12 @@ class StatusComparisonTest { status = createStatus(), isExpanded = false, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) val viewdata2 = StatusViewData.Concrete( status = createStatus(), isExpanded = false, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) assertEquals(viewdata1, viewdata2) @@ -68,14 +62,12 @@ class StatusComparisonTest { status = createStatus(), isExpanded = true, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) val viewdata2 = StatusViewData.Concrete( status = createStatus(), isExpanded = false, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) assertNotEquals(viewdata1, viewdata2) @@ -87,14 +79,12 @@ class StatusComparisonTest { status = createStatus(content = "whatever"), isExpanded = true, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) val viewdata2 = StatusViewData.Concrete( status = createStatus(), isExpanded = false, isShowingContent = false, - isCollapsible = false, isCollapsed = false ) assertNotEquals(viewdata1, viewdata2) diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt index 60dda4193..33215e675 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt @@ -1,14 +1,19 @@ package com.keylesspalace.tusky.components.timeline import androidx.paging.PagingSource +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelinePagingSource import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Test +import org.junit.runner.RunWith import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock +import org.robolectric.annotation.Config +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) class NetworkTimelinePagingSourceTest { private val status = mockStatusViewData() diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt index f7c998b51..cc6a90bd9 100644 --- a/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/StatusMocker.kt @@ -1,6 +1,5 @@ package com.keylesspalace.tusky.components.timeline -import android.text.SpannedString import com.google.gson.Gson import com.keylesspalace.tusky.db.TimelineStatusWithAccount import com.keylesspalace.tusky.entity.Status @@ -25,7 +24,7 @@ fun mockStatus(id: String = "100") = Status( inReplyToId = null, inReplyToAccountId = null, reblog = null, - content = SpannedString("Test"), + content = "Test", createdAt = fixedDate, emojis = emptyList(), reblogsCount = 1, @@ -50,7 +49,6 @@ fun mockStatusViewData(id: String = "100") = StatusViewData.Concrete( status = mockStatus(id), isExpanded = false, isShowingContent = false, - isCollapsible = false, isCollapsed = true, )