Add support for muting conversations (#1732)

* Add support for muting conversations
Implements #1731

* Fix CI

* Apply code review feedback
This commit is contained in:
Levi Bard 2020-03-24 21:06:04 +01:00 committed by GitHub
parent 8e54e4ae16
commit 8cb83050ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 904 additions and 19 deletions

View file

@ -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')"
]
}
}

View file

@ -8,6 +8,7 @@ import com.keylesspalace.tusky.entity.Status
data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Dispatchable data class FavoriteEvent(val statusId: String, val favourite: Boolean) : Dispatchable
data class ReblogEvent(val statusId: String, val reblog: Boolean) : Dispatchable data class ReblogEvent(val statusId: String, val reblog: Boolean) : Dispatchable
data class BookmarkEvent(val statusId: String, val bookmark: 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 UnfollowEvent(val accountId: String) : Dispatchable
data class BlockEvent(val accountId: String) : Dispatchable data class BlockEvent(val accountId: String) : Dispatchable
data class MuteEvent(val accountId: String) : Dispatchable data class MuteEvent(val accountId: String) : Dispatchable

View file

@ -157,6 +157,7 @@ data class ConversationStatusEntity(
mentions = mentions, mentions = mentions,
application = null, application = null,
pinned = false, pinned = false,
muted = false,
poll = poll, poll = poll,
card = null) card = null)
} }

View file

@ -213,6 +213,18 @@ class SearchViewModel @Inject constructor(
search(currentQuery) search(currentQuery)
} }
fun muteConversation(status: Pair<Status, StatusViewData.Concrete>, 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 { companion object {
private const val TAG = "SearchViewModel" private const val TAG = "SearchViewModel"

View file

@ -49,6 +49,7 @@ import com.keylesspalace.tusky.components.search.adapter.SearchStatusesAdapter
import com.keylesspalace.tusky.db.AccountEntity import com.keylesspalace.tusky.db.AccountEntity
import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.entity.Status.Mention
import com.keylesspalace.tusky.interfaces.AccountSelectionListener import com.keylesspalace.tusky.interfaces.AccountSelectionListener
import com.keylesspalace.tusky.interfaces.StatusActionListener import com.keylesspalace.tusky.interfaces.StatusActionListener
import com.keylesspalace.tusky.util.CardViewMode import com.keylesspalace.tusky.util.CardViewMode
@ -228,12 +229,9 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
val loggedInAccountId = viewModel.activeAccount?.accountId val loggedInAccountId = viewModel.activeAccount?.accountId
val popup = PopupMenu(view.context, view) val popup = PopupMenu(view.context, view)
val statusIsByCurrentUser = loggedInAccountId?.equals(accountId) == true
// Give a different menu depending on whether this is the user's own toot or not. // Give a different menu depending on whether this is the user's own toot or not.
if (loggedInAccountId == null || loggedInAccountId != accountId) { if (statusIsByCurrentUser) {
popup.inflate(R.menu.status_more)
val menu = popup.menu
menu.findItem(R.id.status_download_media).isVisible = status.attachments.isNotEmpty()
} else {
popup.inflate(R.menu.status_more_for_user) popup.inflate(R.menu.status_more_for_user)
val menu = popup.menu val menu = popup.menu
menu.findItem(R.id.status_open_as).isVisible = !statusUrl.isNullOrBlank() menu.findItem(R.id.status_open_as).isVisible = !statusUrl.isNullOrBlank()
@ -251,6 +249,10 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
Status.Visibility.UNKNOWN, Status.Visibility.DIRECT -> { Status.Visibility.UNKNOWN, Status.Visibility.DIRECT -> {
} //Ignore } //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) val openAsItem = popup.menu.findItem(R.id.status_open_as)
@ -266,6 +268,19 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
} }
openAsItem.title = openAsTitle openAsItem.title = openAsTitle
val mutable = statusIsByCurrentUser || accountIsInMentions(viewModel.activeAccount, status.mentions)
val muteConversationItem = popup.menu.findItem(R.id.status_mute_conversation).apply {
isVisible = mutable
}
if (mutable) {
muteConversationItem.setTitle(
if (status.muted == true) {
R.string.action_unmute_conversation
} else {
R.string.action_mute_conversation
})
}
popup.setOnMenuItemClickListener { item -> popup.setOnMenuItemClickListener { item ->
when (item.itemId) { when (item.itemId) {
R.id.status_share_content -> { R.id.status_share_content -> {
@ -303,6 +318,12 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
requestDownloadAllMedia(status) requestDownloadAllMedia(status)
return@setOnMenuItemClickListener true return@setOnMenuItemClickListener true
} }
R.id.status_mute_conversation -> {
searchAdapter.getItem(position)?.let { foundStatus ->
viewModel.muteConversation(foundStatus, status.muted != true)
}
return@setOnMenuItemClickListener true
}
R.id.status_mute -> { R.id.status_mute -> {
viewModel.muteAcount(accountId) viewModel.muteAcount(accountId)
return@setOnMenuItemClickListener true return@setOnMenuItemClickListener true
@ -341,6 +362,12 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
popup.show() popup.show()
} }
private fun accountIsInMentions(account: AccountEntity?, mentions: Array<Mention>): Boolean {
return mentions.firstOrNull {
account?.username == it.username && account.domain == Uri.parse(it.url)?.host
} != null
}
private fun showOpenAsDialog(statusUrl: String, dialogTitle: CharSequence) { private fun showOpenAsDialog(statusUrl: String, dialogTitle: CharSequence) {
bottomSheetActivity?.showAccountChooserDialog(dialogTitle, false, object : AccountSelectionListener { bottomSheetActivity?.showAccountChooserDialog(dialogTitle, false, object : AccountSelectionListener {
override fun onAccountSelected(account: AccountEntity) { override fun onAccountSelected(account: AccountEntity) {

View file

@ -30,7 +30,7 @@ import androidx.annotation.NonNull;
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class, @Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class TimelineAccountEntity.class, ConversationEntity.class
}, version = 22) }, version = 23)
public abstract class AppDatabase extends RoomDatabase { public abstract class AppDatabase extends RoomDatabase {
public abstract TootDao tootDao(); 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");
}
};
} }

View file

@ -51,7 +51,8 @@ data class TimelineStatusEntity(
val application: String?, val application: String?,
val reblogServerId: String?, // if it has a reblogged status, it's id is stored here val reblogServerId: String?, // if it has a reblogged status, it's id is stored here
val reblogAccountId: String?, val reblogAccountId: String?,
val poll: String? val poll: String?,
val muted: Boolean?
) )
@Entity( @Entity(

View file

@ -79,7 +79,8 @@ class AppModule {
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13, 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_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22) AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22,
AppDatabase.MIGRATION_22_23)
.build() .build()
} }

View file

@ -43,6 +43,7 @@ data class Status(
val mentions: Array<Mention>, val mentions: Array<Mention>,
val application: Application?, val application: Application?,
var pinned: Boolean?, var pinned: Boolean?,
var muted: Boolean?,
val poll: Poll?, val poll: Poll?,
val card: Card? val card: Card?
) { ) {

View file

@ -185,11 +185,8 @@ public abstract class SFragment extends BaseFragment implements Injectable {
PopupMenu popup = new PopupMenu(getContext(), view); PopupMenu popup = new PopupMenu(getContext(), view);
// Give a different menu depending on whether this is the user's own toot or not. // Give a different menu depending on whether this is the user's own toot or not.
if (loggedInAccountId == null || !loggedInAccountId.equals(accountId)) { boolean statusIsByCurrentUser = loggedInAccountId != null && loggedInAccountId.equals(accountId);
popup.inflate(R.menu.status_more); if (statusIsByCurrentUser) {
Menu menu = popup.getMenu();
menu.findItem(R.id.status_download_media).setVisible(!status.getAttachments().isEmpty());
} else {
popup.inflate(R.menu.status_more_for_user); popup.inflate(R.menu.status_more_for_user);
Menu menu = popup.getMenu(); Menu menu = popup.getMenu();
switch (status.getVisibility()) { switch (status.getVisibility()) {
@ -208,6 +205,10 @@ public abstract class SFragment extends BaseFragment implements Injectable {
break; 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(); Menu menu = popup.getMenu();
@ -231,6 +232,15 @@ public abstract class SFragment extends BaseFragment implements Injectable {
} }
openAsItem.setTitle(openAsTitle); 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 -> { popup.setOnMenuItemClickListener(item -> {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.status_share_content: { case R.id.status_share_content: {
@ -305,12 +315,35 @@ public abstract class SFragment extends BaseFragment implements Injectable {
timelineCases.pin(status, !status.isPinned()); timelineCases.pin(status, !status.isPinned());
return true; 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; return false;
}); });
popup.show(); 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) { protected void viewMedia(int urlIndex, Status status, @Nullable View view) {
final Status actionable = status.getActionableStatus(); final Status actionable = status.getActionableStatus();
final Attachment active = actionable.getAttachments().get(urlIndex); final Attachment active = actionable.getAttachments().get(urlIndex);

View file

@ -53,6 +53,7 @@ import com.keylesspalace.tusky.appstore.BookmarkEvent;
import com.keylesspalace.tusky.appstore.DomainMuteEvent; import com.keylesspalace.tusky.appstore.DomainMuteEvent;
import com.keylesspalace.tusky.appstore.EventHub; import com.keylesspalace.tusky.appstore.EventHub;
import com.keylesspalace.tusky.appstore.FavoriteEvent; import com.keylesspalace.tusky.appstore.FavoriteEvent;
import com.keylesspalace.tusky.appstore.MuteConversationEvent;
import com.keylesspalace.tusky.appstore.MuteEvent; import com.keylesspalace.tusky.appstore.MuteEvent;
import com.keylesspalace.tusky.appstore.PreferenceChangedEvent; import com.keylesspalace.tusky.appstore.PreferenceChangedEvent;
import com.keylesspalace.tusky.appstore.ReblogEvent; import com.keylesspalace.tusky.appstore.ReblogEvent;
@ -503,6 +504,9 @@ public class TimelineFragment extends SFragment implements
} else if (event instanceof BookmarkEvent) { } else if (event instanceof BookmarkEvent) {
BookmarkEvent bookmarkEvent = (BookmarkEvent) event; BookmarkEvent bookmarkEvent = (BookmarkEvent) event;
handleBookmarkEvent(bookmarkEvent); handleBookmarkEvent(bookmarkEvent);
} else if (event instanceof MuteConversationEvent) {
MuteConversationEvent muteEvent = (MuteConversationEvent) event;
handleMuteConversationEvent(muteEvent);
} else if (event instanceof UnfollowEvent) { } else if (event instanceof UnfollowEvent) {
if (kind == Kind.HOME) { if (kind == Kind.HOME) {
String id = ((UnfollowEvent) event).getAccountId(); String id = ((UnfollowEvent) event).getAccountId();
@ -1313,6 +1317,10 @@ public class TimelineFragment extends SFragment implements
setBookmarkForStatus(pos, status, bookmarkEvent.getBookmark()); setBookmarkForStatus(pos, status, bookmarkEvent.getBookmark());
} }
private void handleMuteConversationEvent(@NonNull MuteConversationEvent event) {
fullyRefresh();
}
private void handleStatusComposeEvent(@NonNull Status status) { private void handleStatusComposeEvent(@NonNull Status status) {
switch (kind) { switch (kind) {
case HOME: case HOME:

View file

@ -200,6 +200,16 @@ interface MastodonApi {
@Path("id") statusId: String @Path("id") statusId: String
): Single<Status> ): Single<Status>
@POST("api/v1/statuses/{id}/mute")
fun muteConversation(
@Path("id") statusId: String
): Single<Status>
@POST("api/v1/statuses/{id}/unmute")
fun unmuteConversation(
@Path("id") statusId: String
): Single<Status>
@GET("api/v1/scheduled_statuses") @GET("api/v1/scheduled_statuses")
fun scheduledStatuses( fun scheduledStatuses(
@Query("limit") limit: Int? = null, @Query("limit") limit: Int? = null,

View file

@ -41,7 +41,7 @@ interface TimelineCases {
fun delete(id: String): Single<DeletedStatus> fun delete(id: String): Single<DeletedStatus>
fun pin(status: Status, pin: Boolean) fun pin(status: Status, pin: Boolean)
fun voteInPoll(status: Status, choices: List<Int>): Single<Poll> fun voteInPoll(status: Status, choices: List<Int>): Single<Poll>
fun muteConversation(status: Status, mute: Boolean): Single<Status>
} }
class TimelineCasesImpl( class TimelineCasesImpl(
@ -94,6 +94,19 @@ class TimelineCasesImpl(
} }
} }
override fun muteConversation(status: Status, mute: Boolean): Single<Status> {
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) { override fun mute(id: String) {
val call = mastodonApi.muteAccount(id) val call = mastodonApi.muteAccount(id)
call.enqueue(object : Callback<Relationship> { call.enqueue(object : Callback<Relationship> {

View file

@ -229,6 +229,7 @@ class TimelineRepositoryImpl(
mentions = mentions, mentions = mentions,
application = application, application = application,
pinned = false, pinned = false,
muted = status.muted,
poll = poll, poll = poll,
card = null card = null
) )
@ -256,6 +257,7 @@ class TimelineRepositoryImpl(
mentions = arrayOf(), mentions = arrayOf(),
application = null, application = null,
pinned = false, pinned = false,
muted = status.muted,
poll = null, poll = null,
card = null card = null
) )
@ -282,6 +284,7 @@ class TimelineRepositoryImpl(
mentions = mentions, mentions = mentions,
application = application, application = application,
pinned = false, pinned = false,
muted = status.muted,
poll = poll, poll = poll,
card = null card = null
) )
@ -353,7 +356,8 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
application = null, application = null,
reblogServerId = null, reblogServerId = null,
reblogAccountId = null, reblogAccountId = null,
poll = null poll = null,
muted = false
) )
} }
@ -384,7 +388,8 @@ fun Status.toEntity(timelineUserId: Long,
application = actionable.let(gson::toJson), application = actionable.let(gson::toJson),
reblogServerId = reblog?.id, reblogServerId = reblog?.id,
reblogAccountId = reblog?.let { this.account.id }, reblogAccountId = reblog?.let { this.account.id },
poll = actionable.poll.let(gson::toJson) poll = actionable.poll.let(gson::toJson),
muted = actionable.muted
) )
} }

View file

@ -57,6 +57,7 @@ public abstract class StatusViewData {
final boolean reblogged; final boolean reblogged;
final boolean favourited; final boolean favourited;
final boolean bookmarked; final boolean bookmarked;
private final boolean muted;
@Nullable @Nullable
private final String spoilerText; private final String spoilerText;
private final Status.Visibility visibility; private final Status.Visibility visibility;
@ -92,7 +93,7 @@ public abstract class StatusViewData {
private final PollViewData poll; private final PollViewData poll;
private final boolean isBot; 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<Attachment> attachments, @Nullable String spoilerText, Status.Visibility visibility, List<Attachment> attachments,
@Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded, @Nullable String rebloggedByUsername, @Nullable String rebloggedAvatar, boolean sensitive, boolean isExpanded,
boolean isShowingContent, String userFullName, String nickname, String avatar, boolean isShowingContent, String userFullName, String nickname, String avatar,
@ -115,6 +116,7 @@ public abstract class StatusViewData {
this.reblogged = reblogged; this.reblogged = reblogged;
this.favourited = favourited; this.favourited = favourited;
this.bookmarked = bookmarked; this.bookmarked = bookmarked;
this.muted = muted;
this.visibility = visibility; this.visibility = visibility;
this.attachments = attachments; this.attachments = attachments;
this.rebloggedByUsername = rebloggedByUsername; this.rebloggedByUsername = rebloggedByUsername;
@ -161,6 +163,10 @@ public abstract class StatusViewData {
return bookmarked; return bookmarked;
} }
public boolean isMuted() {
return muted;
}
@Nullable @Nullable
public String getSpoilerText() { public String getSpoilerText() {
return spoilerText; return spoilerText;
@ -401,6 +407,7 @@ public abstract class StatusViewData {
private boolean reblogged; private boolean reblogged;
private boolean favourited; private boolean favourited;
private boolean bookmarked; private boolean bookmarked;
private boolean muted;
private String spoilerText; private String spoilerText;
private Status.Visibility visibility; private Status.Visibility visibility;
private List<Attachment> attachments; private List<Attachment> attachments;
@ -437,6 +444,7 @@ public abstract class StatusViewData {
reblogged = viewData.reblogged; reblogged = viewData.reblogged;
favourited = viewData.favourited; favourited = viewData.favourited;
bookmarked = viewData.bookmarked; bookmarked = viewData.bookmarked;
muted = viewData.muted;
spoilerText = viewData.spoilerText; spoilerText = viewData.spoilerText;
visibility = viewData.visibility; visibility = viewData.visibility;
attachments = viewData.attachments == null ? null : new ArrayList<>(viewData.attachments); attachments = viewData.attachments == null ? null : new ArrayList<>(viewData.attachments);
@ -490,6 +498,11 @@ public abstract class StatusViewData {
return this; return this;
} }
public Builder setMuted(boolean muted) {
this.muted = muted;
return this;
}
public Builder setSpoilerText(String spoilerText) { public Builder setSpoilerText(String spoilerText) {
this.spoilerText = spoilerText; this.spoilerText = spoilerText;
return this; return this;
@ -639,7 +652,7 @@ public abstract class StatusViewData {
if (this.accountEmojis == null) accountEmojis = Collections.emptyList(); if (this.accountEmojis == null) accountEmojis = Collections.emptyList();
if (this.createdAt == null) createdAt = new Date(); 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, visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded,
isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount, isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount,
favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application,

View file

@ -21,6 +21,9 @@
<item <item
android:id="@+id/status_download_media" android:id="@+id/status_download_media"
android:title="@string/download_media" /> android:title="@string/download_media" />
<item
android:id="@+id/status_mute_conversation"
android:title="@string/action_mute_conversation" />
<item <item
android:id="@+id/status_mute" android:id="@+id/status_mute"
android:title="@string/action_mute" /> android:title="@string/action_mute" />

View file

@ -26,6 +26,9 @@
android:id="@+id/status_unreblog_private" android:id="@+id/status_unreblog_private"
android:title="@string/unreblog_private" android:title="@string/unreblog_private"
android:visible="false" /> android:visible="false" />
<item
android:id="@+id/status_mute_conversation"
android:title="@string/action_mute_conversation" />
<item <item
android:id="@+id/status_delete" android:id="@+id/status_delete"
android:title="@string/action_delete" /> android:title="@string/action_delete" />

View file

@ -109,6 +109,8 @@
<string name="action_mute">Mute</string> <string name="action_mute">Mute</string>
<string name="action_unmute">Unmute</string> <string name="action_unmute">Unmute</string>
<string name="action_mute_domain">Mute %s</string> <string name="action_mute_domain">Mute %s</string>
<string name="action_mute_conversation">Mute conversation</string>
<string name="action_unmute_conversation">Unmute conversation</string>
<string name="action_mention">Mention</string> <string name="action_mention">Mention</string>
<string name="action_hide_media">Hide media</string> <string name="action_hide_media">Hide media</string>
<string name="action_open_drawer">Open drawer</string> <string name="action_open_drawer">Open drawer</string>

View file

@ -88,6 +88,7 @@ class BottomSheetActivityTest {
arrayOf(), arrayOf(),
null, null,
pinned = false, pinned = false,
muted = false,
poll = null, poll = null,
card = null card = null
) )

View file

@ -214,6 +214,7 @@ class FilterTest {
mentions = emptyArray(), mentions = emptyArray(),
application = null, application = null,
pinned = false, pinned = false,
muted = false,
poll = if (pollOptions != null) { poll = if (pollOptions != null) {
Poll( Poll(
id = "1234", id = "1234",

View file

@ -314,6 +314,7 @@ class TimelineRepositoryTest {
inReplyToAccountId = null, inReplyToAccountId = null,
inReplyToId = null, inReplyToId = null,
pinned = false, pinned = false,
muted = false,
reblog = null, reblog = null,
url = "http://example.com/statuses/$id", url = "http://example.com/statuses/$id",
poll = null, poll = null,