Implement muting/unmuting conversations, fix possible appearing of muted users in notifications

This commit is contained in:
Alibek Omarov 2020-01-26 16:13:22 +03:00
parent ff361c1743
commit 8cb743efaa
15 changed files with 147 additions and 35 deletions

View File

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

View File

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

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

View File

@ -104,4 +104,4 @@ class NetworkModule {
@Provides
@Singleton
fun providesApi(retrofit: Retrofit): MastodonApi = retrofit.create(MastodonApi::class.java)
}
}

View File

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

View File

@ -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<Integer, Notification> 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) {

View File

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

View File

@ -206,7 +206,17 @@ interface MastodonApi {
fun unpinStatus(
@Path("id") statusId: String
): Single<Status>
@POST("api/v1/statuses/{id}/mute")
fun muteStatus(
@Path("id") statusId: String
): Single<Status>
@POST("api/v1/statuses/{id}/unmute")
fun unmuteStatus(
@Path("id") statusId: String
): Single<Status>
@GET("api/v1/scheduled_statuses")
fun scheduledStatuses(
@Query("limit") limit: Int? = null,

View File

@ -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<Status>
fun favourite(status: Status, favourite: Boolean): Single<Status>
fun bookmark(status: Status, bookmark: Boolean): Single<Status>
fun muteStatus(status: Status, mute: Boolean)
fun mute(id: String)
fun block(id: String)
fun delete(id: String): Single<DeletedStatus>
@ -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(
}
}
}
}

View File

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

View File

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

View File

@ -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<Attachment> 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<Emoji> statusEmojis, List<Emoji> 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);
}
}
}

View File

@ -21,6 +21,14 @@
<item
android:id="@+id/status_open_as"
android:title="@string/action_open_as" />
<item
android:id="@+id/status_mute_conversation"
android:title="@string/action_mute_conversation"
android:visible="false" />
<item
android:id="@+id/status_unmute_conversation"
android:title="@string/action_unmute_conversation"
android:visible="false" />
<item
android:id="@+id/status_download_media"
android:title="@string/download_media" />

View File

@ -21,6 +21,14 @@
<item
android:id="@+id/status_open_as"
android:title="@string/action_open_as" />
<item
android:id="@+id/status_mute_conversation"
android:title="@string/action_mute_conversation"
android:visible="false" />
<item
android:id="@+id/status_unmute_conversation"
android:title="@string/action_unmute_conversation"
android:visible="false" />
<item
android:id="@+id/status_reblog_private"
android:title="@string/reblog_private"

View File

@ -2,7 +2,8 @@
<string name="action_reply_to">Reply to</string>
<string name="action_markdown">Markdown</string>
<!--<string name="action_mute">-->
<string name="action_mute_conversation">Mute conversation</string>
<string name="action_unmute_conversation">Unmute conversation</string>
<string name="hint_appname">Application name</string>
<string name="hint_website">Application website</string>