From 8cb743efaaadef57abbad2b024332d7a39af36d2 Mon Sep 17 00:00:00 2001 From: Alibek Omarov Date: Sun, 26 Jan 2020 16:13:22 +0300 Subject: [PATCH] Implement muting/unmuting conversations, fix possible appearing of muted users in notifications --- .../tusky/adapter/NotificationsAdapter.java | 2 +- .../tusky/appstore/CacheUpdater.kt | 4 +- .../keylesspalace/tusky/appstore/Events.kt | 1 + .../keylesspalace/tusky/di/NetworkModule.kt | 2 +- .../com/keylesspalace/tusky/entity/Status.kt | 16 ++++++-- .../tusky/fragment/NotificationsFragment.java | 40 ++++++++++++++----- .../tusky/fragment/SFragment.java | 22 ++++++++++ .../tusky/network/MastodonApi.kt | 12 +++++- .../tusky/network/TimelineCases.kt | 16 +++++++- .../tusky/util/NotificationHelper.java | 8 ++-- .../tusky/util/ViewDataUtils.java | 3 +- .../tusky/viewdata/StatusViewData.java | 37 ++++++++++++----- app/src/main/res/menu/status_more.xml | 8 ++++ .../main/res/menu/status_more_for_user.xml | 8 ++++ app/src/main/res/values/husky.xml | 3 +- 15 files changed, 147 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 9f7fe665..5c526d4e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -259,7 +259,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter { switch (concrete.getType()) { case MENTION: case POLL: { - if(concrete.getStatusViewData() != null && concrete.getStatusViewData().isMuted()) + if(concrete.getStatusViewData() != null && concrete.getStatusViewData().isThreadMuted()) return VIEW_TYPE_MUTED_STATUS; return VIEW_TYPE_STATUS; } diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt index 6404de5c..d0da9091 100644 --- a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt @@ -27,7 +27,7 @@ class CacheUpdater @Inject constructor( is ReblogEvent -> timelineDao.setReblogged(accountId, event.statusId, event.reblog) is BookmarkEvent -> - timelineDao.setBookmarked(accountId, event.statusId, event.bookmark ) + timelineDao.setBookmarked(accountId, event.statusId, event.bookmark) is UnfollowEvent -> timelineDao.removeAllByUser(accountId, event.accountId) is StatusDeletedEvent -> @@ -52,4 +52,4 @@ class CacheUpdater @Inject constructor( .subscribeOn(Schedulers.io()) .subscribe() } -} \ 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 6c82d16f..b856b9e6 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 MuteStatusEvent(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/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt index 26309be5..d84d429e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -104,4 +104,4 @@ class NetworkModule { @Provides @Singleton fun providesApi(retrofit: Retrofit): MastodonApi = retrofit.create(MastodonApi::class.java) -} \ No newline at end of file +} 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 89caecf7..b165bc8c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -46,7 +46,8 @@ data class Status( val poll: Poll?, val card: Card?, var content_type: String? = null, - val pleroma: PleromaStatus? = null + val pleroma: PleromaStatus? = null, + var muted: Boolean = false /* set when either thread or user is muted */ ) { val actionableId: String @@ -125,9 +126,18 @@ data class Status( ) } - fun isMuted(): Boolean { + fun isUserMuted(): Boolean { + return muted && !isThreadMuted() + } + + fun isThreadMuted(): Boolean { return pleroma?.threadMuted ?: false } + + fun setThreadMuted(mute: Boolean) { + if(pleroma?.threadMuted != null) + pleroma.threadMuted = mute + } private fun getEditableText(): String { val builder = SpannableStringBuilder(content) @@ -158,7 +168,7 @@ data class Status( } data class PleromaStatus( - @SerializedName("thread_muted") val threadMuted: Boolean? + @SerializedName("thread_muted") var threadMuted: Boolean? ) data class Mention ( diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 52e2ee2b..482bd698 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -54,12 +54,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.adapter.NotificationsAdapter; import com.keylesspalace.tusky.adapter.StatusBaseViewHolder; -import com.keylesspalace.tusky.appstore.BlockEvent; -import com.keylesspalace.tusky.appstore.BookmarkEvent; -import com.keylesspalace.tusky.appstore.EventHub; -import com.keylesspalace.tusky.appstore.FavoriteEvent; -import com.keylesspalace.tusky.appstore.PreferenceChangedEvent; -import com.keylesspalace.tusky.appstore.ReblogEvent; +import com.keylesspalace.tusky.appstore.*; import com.keylesspalace.tusky.db.AccountEntity; import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.di.Injectable; @@ -329,6 +324,15 @@ public class NotificationsFragment extends SFragment implements posAndNotification.second.getStatus(), event.getReblog()); } + + private void handleMuteStatusEvent(MuteStatusEvent event) { + Pair posAndNotification = findReplyPosition(event.getStatusId()); + if (posAndNotification == null) return; + //noinspection ConstantConditions + setMutedStatusForStatus(posAndNotification.first, + posAndNotification.second.getStatus(), + event.getMute()); + } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { @@ -381,6 +385,8 @@ public class NotificationsFragment extends SFragment implements handleBookmarkEvent((BookmarkEvent) event); } else if (event instanceof ReblogEvent) { handleReblogEvent((ReblogEvent) event); + } else if (event instanceof MuteStatusEvent) { + handleMuteStatusEvent((MuteStatusEvent) event); } else if (event instanceof BlockEvent) { removeAllByAccountId(((BlockEvent) event).getAccountId()); } else if (event instanceof PreferenceChangedEvent) { @@ -441,7 +447,7 @@ public class NotificationsFragment extends SFragment implements notifications.setPairedItem(position, newViewData); updateAdapter(); } - + @Override public void onFavourite(final boolean favourite, final int position) { final Notification notification = notifications.get(position).asRight(); @@ -600,14 +606,30 @@ public class NotificationsFragment extends SFragment implements (NotificationViewData.Concrete) notifications.getPairedItem(position); StatusViewData.Concrete statusViewData = new StatusViewData.Builder(old.getStatusViewData()) - .setMuted(isMuted) + .setThreadMuted(isMuted) .createStatusViewData(); - Log.d("ASDASDASD", "position = " + position + " isMuted = " + isMuted); NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(), old.getId(), old.getAccount(), statusViewData, old.isExpanded()); notifications.setPairedItem(position, notificationViewData); updateAdapter(); } + + private void setMutedStatusForStatus(int position, Status status, boolean muted) { + status.setThreadMuted(muted); + + NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position); + + StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData()); + viewDataBuilder.setThreadMuted(muted); + + NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete( + viewdata.getType(), viewdata.getId(), viewdata.getAccount(), + viewDataBuilder.createStatusViewData(), viewdata.isExpanded()); + + notifications.setPairedItem(position, newViewData); + updateAdapter(); + } + @Override public void onLoadMore(int position) { 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 59295ca6..08fcf459 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java @@ -240,6 +240,20 @@ public abstract class SFragment extends BaseFragment implements Injectable { replyToItem.setVisible(false); } + + // maybe not a best check + if(status.getPleroma() != null) { + boolean showMute = true; // predict state + + if(status.isThreadMuted() == true) { + showMute = false; + } + + // show mutes only for Pleroma because Mastodon don't handle them in sane way + // e.g. why you can only mute threads where you were participated? + menu.findItem(R.id.status_mute_conversation).setVisible(showMute); + menu.findItem(R.id.status_unmute_conversation).setVisible(!showMute); + } popup.setOnMenuItemClickListener(item -> { switch (item.getItemId()) { @@ -298,6 +312,14 @@ public abstract class SFragment extends BaseFragment implements Injectable { openReportPage(accountId, accountUsername, id); return true; } + case R.id.status_mute_conversation: { + timelineCases.muteStatus(status, true); + return true; + } + case R.id.status_unmute_conversation: { + timelineCases.muteStatus(status, false); + return true; + } case R.id.status_unreblog_private: { onReblog(false, position); return true; 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 48931170..2dacaec9 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -206,7 +206,17 @@ interface MastodonApi { fun unpinStatus( @Path("id") statusId: String ): Single - + + @POST("api/v1/statuses/{id}/mute") + fun muteStatus( + @Path("id") statusId: String + ): Single + + @POST("api/v1/statuses/{id}/unmute") + fun unmuteStatus( + @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..c2c0f988 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt @@ -27,6 +27,7 @@ import retrofit2.Call import retrofit2.Callback import retrofit2.Response import java.lang.IllegalStateException +import android.util.Log /** * Created by charlag on 3/24/18. @@ -36,6 +37,7 @@ interface TimelineCases { fun reblog(status: Status, reblog: Boolean): Single fun favourite(status: Status, favourite: Boolean): Single fun bookmark(status: Status, bookmark: Boolean): Single + fun muteStatus(status: Status, mute: Boolean) fun mute(id: String) fun block(id: String) fun delete(id: String): Single @@ -103,6 +105,18 @@ class TimelineCasesImpl( }) eventHub.dispatch(MuteEvent(id)) } + + override fun muteStatus(status: Status, mute: Boolean) { + val id = status.actionableId + + (if (mute) { + mastodonApi.muteStatus(id) + } else { + mastodonApi.unmuteStatus(id) + }).subscribe( { status -> + eventHub.dispatch(MuteStatusEvent(status.id, mute)) + }, {}) + } override fun block(id: String) { val call = mastodonApi.blockAccount(id) @@ -143,4 +157,4 @@ class TimelineCasesImpl( } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/NotificationHelper.java index 753850c7..b066ee6b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/NotificationHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/NotificationHelper.java @@ -140,13 +140,13 @@ public class NotificationHelper { } // Pleroma extension: don't notify about seen notifications - if (body.getPleroma() != null && body.getPleroma().getSeen()) { + if (body.getPleroma() != null && body.getPleroma().getSeen() == true) { return; } - if (body.getStatus() != null - && body.getStatus().getPleroma() != null - && body.getStatus().getPleroma().getThreadMuted() == true) { + if (body.getStatus() != null && + (body.getStatus().isUserMuted() == true || + body.getStatus().isThreadMuted() == true)) { return; } diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java index 979a0cb1..28e553a0 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java @@ -65,7 +65,8 @@ public final class ViewDataUtils { .setPoll(visibleStatus.getPoll()) .setCard(visibleStatus.getCard()) .setIsBot(visibleStatus.getAccount().getBot()) - .setMuted(visibleStatus.isMuted()) + .setUserMuted(visibleStatus.isUserMuted()) + .setThreadMuted(visibleStatus.isThreadMuted()) .createStatusViewData(); } 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 a5ca094e..014c5d8d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java @@ -91,7 +91,8 @@ public abstract class StatusViewData { @Nullable private final PollViewData poll; private final boolean isBot; - private final boolean isMuted; + private final boolean isThreadMuted; + private final boolean isUserMuted; public Concrete(String id, Spanned content, boolean reblogged, boolean favourited, boolean bookmarked, @Nullable String spoilerText, Status.Visibility visibility, List attachments, @@ -100,7 +101,8 @@ public abstract class StatusViewData { Date createdAt, int reblogsCount, int favouritesCount, @Nullable String inReplyToId, @Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled, Status.Application application, List statusEmojis, List accountEmojis, @Nullable Card card, - boolean isCollapsible, boolean isCollapsed, @Nullable PollViewData poll, boolean isBot, boolean isMuted) { + boolean isCollapsible, boolean isCollapsed, @Nullable PollViewData poll, boolean isBot, boolean isThreadMuted, + boolean isUserMuted) { this.id = id; if (Build.VERSION.SDK_INT == 23) { @@ -140,7 +142,8 @@ public abstract class StatusViewData { this.isCollapsed = isCollapsed; this.poll = poll; this.isBot = isBot; - this.isMuted = isMuted; + this.isThreadMuted = isThreadMuted; + this.isUserMuted = isUserMuted; } public String getId() { @@ -289,8 +292,12 @@ public abstract class StatusViewData { return id.hashCode(); } - public boolean isMuted() { - return isMuted; + public boolean isThreadMuted() { + return isThreadMuted; + } + + public boolean isUserMuted() { + return isUserMuted; } public boolean deepEquals(StatusViewData o) { @@ -327,7 +334,8 @@ public abstract class StatusViewData { Objects.equals(card, concrete.card) && Objects.equals(poll, concrete.poll) && isCollapsed == concrete.isCollapsed && - isMuted == concrete.isMuted; + isThreadMuted == concrete.isThreadMuted && + isUserMuted == concrete.isUserMuted; } static Spanned replaceCrashingCharacters(Spanned content) { @@ -434,7 +442,8 @@ public abstract class StatusViewData { private boolean isCollapsed; /** Whether the status is shown partially or fully */ private PollViewData poll; private boolean isBot; - private boolean isMuted; + private boolean isThreadMuted; + private boolean isUserMuted; public Builder() { } @@ -471,7 +480,8 @@ public abstract class StatusViewData { isCollapsed = viewData.isCollapsed(); poll = viewData.poll; isBot = viewData.isBot(); - isMuted = viewData.isMuted; + isThreadMuted = viewData.isThreadMuted; + isUserMuted = viewData.isUserMuted; } public Builder setId(String id) { @@ -643,8 +653,13 @@ public abstract class StatusViewData { return this; } - public Builder setMuted(Boolean isMuted) { - this.isMuted = isMuted; + public Builder setUserMuted(Boolean isUserMuted) { + this.isUserMuted = isUserMuted; + return this; + } + + public Builder setThreadMuted(Boolean isThreadMuted) { + this.isThreadMuted = isThreadMuted; return this; } @@ -657,7 +672,7 @@ public abstract class StatusViewData { visibility, attachments, rebloggedByUsername, rebloggedAvatar, isSensitive, isExpanded, isShowingContent, userFullName, nickname, avatar, createdAt, reblogsCount, favouritesCount, inReplyToId, mentions, senderId, rebloggingEnabled, application, - statusEmojis, accountEmojis, card, isCollapsible, isCollapsed, poll, isBot, isMuted); + statusEmojis, accountEmojis, card, isCollapsible, isCollapsed, poll, isBot, isThreadMuted, isUserMuted); } } } diff --git a/app/src/main/res/menu/status_more.xml b/app/src/main/res/menu/status_more.xml index 9c4b0685..b68e61e4 100644 --- a/app/src/main/res/menu/status_more.xml +++ b/app/src/main/res/menu/status_more.xml @@ -21,6 +21,14 @@ + + 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 3fda3a9c..1e3d9454 100644 --- a/app/src/main/res/menu/status_more_for_user.xml +++ b/app/src/main/res/menu/status_more_for_user.xml @@ -21,6 +21,14 @@ + + Reply to Markdown - + Mute conversation + Unmute conversation Application name Application website