From 8cb83050acdaa98582e0a617309a73d377061eca Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Tue, 24 Mar 2020 21:06:04 +0100 Subject: [PATCH] Add support for muting conversations (#1732) * Add support for muting conversations Implements #1731 * Fix CI * Apply code review feedback --- .../23.json | 741 ++++++++++++++++++ .../keylesspalace/tusky/appstore/Events.kt | 1 + .../conversation/ConversationEntity.kt | 1 + .../components/search/SearchViewModel.kt | 12 + .../fragments/SearchStatusesFragment.kt | 37 +- .../keylesspalace/tusky/db/AppDatabase.java | 9 +- .../tusky/db/TimelineStatusEntity.kt | 3 +- .../com/keylesspalace/tusky/di/AppModule.kt | 5 +- .../com/keylesspalace/tusky/entity/Status.kt | 1 + .../tusky/fragment/SFragment.java | 43 +- .../tusky/fragment/TimelineFragment.java | 8 + .../tusky/network/MastodonApi.kt | 10 + .../tusky/network/TimelineCases.kt | 15 +- .../tusky/repository/TimelineRepository.kt | 9 +- .../tusky/viewdata/StatusViewData.java | 17 +- app/src/main/res/menu/status_more.xml | 3 + .../main/res/menu/status_more_for_user.xml | 3 + app/src/main/res/values/strings.xml | 2 + .../tusky/BottomSheetActivityTest.kt | 1 + .../com/keylesspalace/tusky/FilterTest.kt | 1 + .../tusky/fragment/TimelineRepositoryTest.kt | 1 + 21 files changed, 904 insertions(+), 19 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/23.json diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/23.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/23.json new file mode 100644 index 00000000..d7f2b297 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/23.json @@ -0,0 +1,741 @@ +{ + "formatVersion": 1, + "database": { + "version": 23, + "identityHash": "03a7436643ef356198742c5f8e054f5f", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "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, `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": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `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": "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, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, 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": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "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 + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` 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.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.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, '03a7436643ef356198742c5f8e054f5f')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt index 7bdc17e6..8e62faeb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt @@ -8,6 +8,7 @@ import com.keylesspalace.tusky.entity.Status data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Dispatchable data class ReblogEvent(val statusId: String, val reblog: Boolean) : Dispatchable data class BookmarkEvent(val statusId: String, val bookmark: Boolean) : Dispatchable +data class MuteConversationEvent(val statusId: String, val mute: Boolean) : Dispatchable data class UnfollowEvent(val accountId: String) : Dispatchable data class BlockEvent(val accountId: String) : Dispatchable data class MuteEvent(val accountId: String) : Dispatchable 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 7c4ed108..8ee1a284 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 @@ -157,6 +157,7 @@ data class ConversationStatusEntity( mentions = mentions, application = null, pinned = false, + muted = false, poll = poll, card = null) } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt index 7979fee5..71121a91 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -213,6 +213,18 @@ class SearchViewModel @Inject constructor( search(currentQuery) } + fun muteConversation(status: Pair, mute: Boolean) { + val idx = loadedStatuses.indexOf(status) + if (idx >= 0) { + val newPair = Pair(status.first, StatusViewData.Builder(status.second).setMuted(mute).createStatusViewData()) + loadedStatuses[idx] = newPair + repoResultStatus.value?.refresh?.invoke() + } + timelineCases.muteConversation(status.first, mute) + .onErrorReturnItem(status.first) + .subscribe() + .autoDispose() + } companion object { private const val TAG = "SearchViewModel" diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt index ac171b9e..0833f04c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -49,6 +49,7 @@ import com.keylesspalace.tusky.components.search.adapter.SearchStatusesAdapter import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.Status.Mention import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.util.CardViewMode @@ -228,12 +229,9 @@ class SearchStatusesFragment : SearchFragment { } //Ignore } + } else { + popup.inflate(R.menu.status_more) + val menu = popup.menu + menu.findItem(R.id.status_download_media).isVisible = status.attachments.isNotEmpty() } val openAsItem = popup.menu.findItem(R.id.status_open_as) @@ -266,6 +268,19 @@ class SearchStatusesFragment : SearchFragment when (item.itemId) { R.id.status_share_content -> { @@ -303,6 +318,12 @@ class SearchStatusesFragment : SearchFragment { + searchAdapter.getItem(position)?.let { foundStatus -> + viewModel.muteConversation(foundStatus, status.muted != true) + } + return@setOnMenuItemClickListener true + } R.id.status_mute -> { viewModel.muteAcount(accountId) return@setOnMenuItemClickListener true @@ -341,6 +362,12 @@ class SearchStatusesFragment : SearchFragment): Boolean { + return mentions.firstOrNull { + account?.username == it.username && account.domain == Uri.parse(it.url)?.host + } != null + } + private fun showOpenAsDialog(statusUrl: String, dialogTitle: CharSequence) { bottomSheetActivity?.showAccountChooserDialog(dialogTitle, false, object : AccountSelectionListener { override fun onAccountSelected(account: AccountEntity) { 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 dc32eea7..608333d9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -30,7 +30,7 @@ import androidx.annotation.NonNull; @Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, TimelineAccountEntity.class, ConversationEntity.class - }, version = 22) + }, version = 23) public abstract class AppDatabase extends RoomDatabase { public abstract TootDao tootDao(); @@ -333,4 +333,11 @@ public abstract class AppDatabase extends RoomDatabase { } }; + public static final Migration MIGRATION_22_23 = new Migration(22, 23) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `muted` INTEGER"); + } + }; + } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt index 94111a95..da98cb4b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/TimelineStatusEntity.kt @@ -51,7 +51,8 @@ data class TimelineStatusEntity( val application: String?, val reblogServerId: String?, // if it has a reblogged status, it's id is stored here val reblogAccountId: String?, - val poll: String? + val poll: String?, + val muted: Boolean? ) @Entity( diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index 669bd216..a4f2eb5e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -79,8 +79,9 @@ class AppModule { AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13, AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16, AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, - AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22) - .build() + AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22, + AppDatabase.MIGRATION_22_23) + .build() } @Provides 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 2ccf0dcb..aee30e8d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -43,6 +43,7 @@ data class Status( val mentions: Array, val application: Application?, var pinned: Boolean?, + var muted: Boolean?, val poll: Poll?, val card: Card? ) { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java index 4f795f18..6a7e5f12 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -185,11 +185,8 @@ public abstract class SFragment extends BaseFragment implements Injectable { PopupMenu popup = new PopupMenu(getContext(), view); // Give a different menu depending on whether this is the user's own toot or not. - if (loggedInAccountId == null || !loggedInAccountId.equals(accountId)) { - popup.inflate(R.menu.status_more); - Menu menu = popup.getMenu(); - menu.findItem(R.id.status_download_media).setVisible(!status.getAttachments().isEmpty()); - } else { + boolean statusIsByCurrentUser = loggedInAccountId != null && loggedInAccountId.equals(accountId); + if (statusIsByCurrentUser) { popup.inflate(R.menu.status_more_for_user); Menu menu = popup.getMenu(); switch (status.getVisibility()) { @@ -208,6 +205,10 @@ public abstract class SFragment extends BaseFragment implements Injectable { break; } } + } else { + popup.inflate(R.menu.status_more); + Menu menu = popup.getMenu(); + menu.findItem(R.id.status_download_media).setVisible(!status.getAttachments().isEmpty()); } Menu menu = popup.getMenu(); @@ -231,6 +232,15 @@ public abstract class SFragment extends BaseFragment implements Injectable { } openAsItem.setTitle(openAsTitle); + MenuItem muteConversationItem = menu.findItem(R.id.status_mute_conversation); + boolean mutable = statusIsByCurrentUser || accountIsInMentions(activeAccount, status.getMentions()); + muteConversationItem.setVisible(mutable); + if (mutable) { + muteConversationItem.setTitle((status.getMuted() == null || !status.getMuted()) ? + R.string.action_mute_conversation : + R.string.action_unmute_conversation); + } + popup.setOnMenuItemClickListener(item -> { switch (item.getItemId()) { case R.id.status_share_content: { @@ -305,12 +315,35 @@ public abstract class SFragment extends BaseFragment implements Injectable { timelineCases.pin(status, !status.isPinned()); return true; } + case R.id.status_mute_conversation: { + timelineCases.muteConversation(status, status.getMuted() == null || !status.getMuted()) + .observeOn(AndroidSchedulers.mainThread()) + .as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) + .subscribe(); + return true; + } } return false; }); popup.show(); } + private static boolean accountIsInMentions(AccountEntity account, Status.Mention[] mentions) { + if (account == null) { + return false; + } + + for (Status.Mention mention : mentions) { + if (account.getUsername().equals(mention.getUsername())) { + Uri uri = Uri.parse(mention.getUrl()); + if (uri != null && account.getDomain().equals(uri.getHost())) { + return true; + } + } + } + return false; + } + protected void viewMedia(int urlIndex, Status status, @Nullable View view) { final Status actionable = status.getActionableStatus(); final Attachment active = actionable.getAttachments().get(urlIndex); diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java index 8db927b2..0c20cfaf 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java @@ -53,6 +53,7 @@ import com.keylesspalace.tusky.appstore.BookmarkEvent; import com.keylesspalace.tusky.appstore.DomainMuteEvent; import com.keylesspalace.tusky.appstore.EventHub; import com.keylesspalace.tusky.appstore.FavoriteEvent; +import com.keylesspalace.tusky.appstore.MuteConversationEvent; import com.keylesspalace.tusky.appstore.MuteEvent; import com.keylesspalace.tusky.appstore.PreferenceChangedEvent; import com.keylesspalace.tusky.appstore.ReblogEvent; @@ -503,6 +504,9 @@ public class TimelineFragment extends SFragment implements } else if (event instanceof BookmarkEvent) { BookmarkEvent bookmarkEvent = (BookmarkEvent) event; handleBookmarkEvent(bookmarkEvent); + } else if (event instanceof MuteConversationEvent) { + MuteConversationEvent muteEvent = (MuteConversationEvent) event; + handleMuteConversationEvent(muteEvent); } else if (event instanceof UnfollowEvent) { if (kind == Kind.HOME) { String id = ((UnfollowEvent) event).getAccountId(); @@ -1313,6 +1317,10 @@ public class TimelineFragment extends SFragment implements setBookmarkForStatus(pos, status, bookmarkEvent.getBookmark()); } + private void handleMuteConversationEvent(@NonNull MuteConversationEvent event) { + fullyRefresh(); + } + private void handleStatusComposeEvent(@NonNull Status status) { switch (kind) { case HOME: diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 7c4b454a..dc69ed7d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -200,6 +200,16 @@ interface MastodonApi { @Path("id") statusId: String ): Single + @POST("api/v1/statuses/{id}/mute") + fun muteConversation( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/unmute") + fun unmuteConversation( + @Path("id") statusId: String + ): Single + @GET("api/v1/scheduled_statuses") fun scheduledStatuses( @Query("limit") limit: Int? = null, diff --git a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt index ce9dc7a9..50b08698 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt @@ -41,7 +41,7 @@ interface TimelineCases { fun delete(id: String): Single fun pin(status: Status, pin: Boolean) fun voteInPoll(status: Status, choices: List): Single - + fun muteConversation(status: Status, mute: Boolean): Single } class TimelineCasesImpl( @@ -94,6 +94,19 @@ class TimelineCasesImpl( } } + override fun muteConversation(status: Status, mute: Boolean): Single { + val id = status.actionableId + + val call = if (mute) { + mastodonApi.muteConversation(id) + } else { + mastodonApi.unmuteConversation(id) + } + return call.doAfterSuccess { + eventHub.dispatch(MuteConversationEvent(status.id, mute)) + } + } + override fun mute(id: String) { val call = mastodonApi.muteAccount(id) call.enqueue(object : Callback { diff --git a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt index 06c001c9..f16bb542 100644 --- a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt @@ -229,6 +229,7 @@ class TimelineRepositoryImpl( mentions = mentions, application = application, pinned = false, + muted = status.muted, poll = poll, card = null ) @@ -256,6 +257,7 @@ class TimelineRepositoryImpl( mentions = arrayOf(), application = null, pinned = false, + muted = status.muted, poll = null, card = null ) @@ -282,6 +284,7 @@ class TimelineRepositoryImpl( mentions = mentions, application = application, pinned = false, + muted = status.muted, poll = poll, card = null ) @@ -353,7 +356,8 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity { application = null, reblogServerId = null, reblogAccountId = null, - poll = null + poll = null, + muted = false ) } @@ -384,7 +388,8 @@ fun Status.toEntity(timelineUserId: Long, application = actionable.let(gson::toJson), reblogServerId = reblog?.id, reblogAccountId = reblog?.let { this.account.id }, - poll = actionable.poll.let(gson::toJson) + poll = actionable.poll.let(gson::toJson), + muted = actionable.muted ) } diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java index b43c7628..81e10100 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java @@ -57,6 +57,7 @@ public abstract class StatusViewData { final boolean reblogged; final boolean favourited; final boolean bookmarked; + private final boolean muted; @Nullable private final String spoilerText; private final Status.Visibility visibility; @@ -92,7 +93,7 @@ public abstract class StatusViewData { private final PollViewData poll; private final boolean isBot; - public Concrete(String id, Spanned content, boolean reblogged, boolean favourited, boolean bookmarked, + public Concrete(String id, Spanned content, boolean reblogged, boolean favourited, boolean bookmarked, boolean muted, @Nullable String spoilerText, Status.Visibility visibility, List attachments, @Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded, boolean isShowingContent, String userFullName, String nickname, String avatar, @@ -115,6 +116,7 @@ public abstract class StatusViewData { this.reblogged = reblogged; this.favourited = favourited; this.bookmarked = bookmarked; + this.muted = muted; this.visibility = visibility; this.attachments = attachments; this.rebloggedByUsername = rebloggedByUsername; @@ -161,6 +163,10 @@ public abstract class StatusViewData { return bookmarked; } + public boolean isMuted() { + return muted; + } + @Nullable public String getSpoilerText() { return spoilerText; @@ -401,6 +407,7 @@ public abstract class StatusViewData { private boolean reblogged; private boolean favourited; private boolean bookmarked; + private boolean muted; private String spoilerText; private Status.Visibility visibility; private List attachments; @@ -437,6 +444,7 @@ public abstract class StatusViewData { reblogged = viewData.reblogged; favourited = viewData.favourited; bookmarked = viewData.bookmarked; + muted = viewData.muted; spoilerText = viewData.spoilerText; visibility = viewData.visibility; attachments = viewData.attachments == null ? null : new ArrayList<>(viewData.attachments); @@ -490,6 +498,11 @@ public abstract class StatusViewData { return this; } + public Builder setMuted(boolean muted) { + this.muted = muted; + return this; + } + public Builder setSpoilerText(String spoilerText) { this.spoilerText = spoilerText; return this; @@ -639,7 +652,7 @@ public abstract class StatusViewData { if (this.accountEmojis == null) accountEmojis = Collections.emptyList(); if (this.createdAt == null) createdAt = new Date(); - return new StatusViewData.Concrete(id, content, reblogged, favourited, bookmarked, spoilerText, + return new StatusViewData.Concrete(id, content, reblogged, favourited, bookmarked, muted, spoilerText, visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount, favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, diff --git a/app/src/main/res/menu/status_more.xml b/app/src/main/res/menu/status_more.xml index 525ce90a..c73b7e9f 100644 --- a/app/src/main/res/menu/status_more.xml +++ b/app/src/main/res/menu/status_more.xml @@ -21,6 +21,9 @@ + diff --git a/app/src/main/res/menu/status_more_for_user.xml b/app/src/main/res/menu/status_more_for_user.xml index 9b363e49..e4f391ab 100644 --- a/app/src/main/res/menu/status_more_for_user.xml +++ b/app/src/main/res/menu/status_more_for_user.xml @@ -26,6 +26,9 @@ android:id="@+id/status_unreblog_private" android:title="@string/unreblog_private" android:visible="false" /> + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7a91facb..b517db86 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -109,6 +109,8 @@ Mute Unmute Mute %s + Mute conversation + Unmute conversation Mention Hide media Open drawer diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt index 5c92667e..86d0925f 100644 --- a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt @@ -88,6 +88,7 @@ class BottomSheetActivityTest { arrayOf(), null, pinned = false, + muted = false, poll = null, card = null ) diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt index 965eda67..19288499 100644 --- a/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/FilterTest.kt @@ -214,6 +214,7 @@ class FilterTest { mentions = emptyArray(), application = null, pinned = false, + muted = false, poll = if (pollOptions != null) { Poll( id = "1234", diff --git a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt b/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt index b55be090..f80f57b8 100644 --- a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt @@ -314,6 +314,7 @@ class TimelineRepositoryTest { inReplyToAccountId = null, inReplyToId = null, pinned = false, + muted = false, reblog = null, url = "http://example.com/statuses/$id", poll = null,