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 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

View file

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

View file

@ -213,6 +213,18 @@ class SearchViewModel @Inject constructor(
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 {
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.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<Pair<Status, StatusViewData.Concre
val loggedInAccountId = viewModel.activeAccount?.accountId
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.
if (loggedInAccountId == null || loggedInAccountId != accountId) {
popup.inflate(R.menu.status_more)
val menu = popup.menu
menu.findItem(R.id.status_download_media).isVisible = status.attachments.isNotEmpty()
} else {
if (statusIsByCurrentUser) {
popup.inflate(R.menu.status_more_for_user)
val menu = popup.menu
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 -> {
} //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<Pair<Status, StatusViewData.Concre
}
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 ->
when (item.itemId) {
R.id.status_share_content -> {
@ -303,6 +318,12 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
requestDownloadAllMedia(status)
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 -> {
viewModel.muteAcount(accountId)
return@setOnMenuItemClickListener true
@ -341,6 +362,12 @@ class SearchStatusesFragment : SearchFragment<Pair<Status, StatusViewData.Concre
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) {
bottomSheetActivity?.showAccountChooserDialog(dialogTitle, false, object : AccountSelectionListener {
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,
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");
}
};
}

View file

@ -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(

View file

@ -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

View file

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

View file

@ -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);

View file

@ -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:

View file

@ -200,6 +200,16 @@ interface MastodonApi {
@Path("id") statusId: String
): 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")
fun scheduledStatuses(
@Query("limit") limit: Int? = null,

View file

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

View file

@ -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
)
}

View file

@ -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<Attachment> 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<Attachment> 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,

View file

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

View file

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

View file

@ -109,6 +109,8 @@
<string name="action_mute">Mute</string>
<string name="action_unmute">Unmute</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_hide_media">Hide media</string>
<string name="action_open_drawer">Open drawer</string>

View file

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

View file

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

View file

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