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

main
Alibek Omarov 5 years ago
parent 565f7f5788
commit b76d3c3979
  1. 2
      app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
  2. 4
      app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt
  3. 1
      app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt
  4. 2
      app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt
  5. 16
      app/src/main/java/com/keylesspalace/tusky/entity/Status.kt
  6. 40
      app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
  7. 22
      app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
  8. 12
      app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
  9. 16
      app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt
  10. 8
      app/src/main/java/com/keylesspalace/tusky/util/NotificationHelper.java
  11. 3
      app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.java
  12. 37
      app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.java
  13. 8
      app/src/main/res/menu/status_more.xml
  14. 8
      app/src/main/res/menu/status_more_for_user.xml
  15. 3
      app/src/main/res/values/husky.xml

@ -259,7 +259,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
switch (concrete.getType()) { switch (concrete.getType()) {
case MENTION: case MENTION:
case POLL: { case POLL: {
if(concrete.getStatusViewData() != null && concrete.getStatusViewData().isMuted()) if(concrete.getStatusViewData() != null && concrete.getStatusViewData().isThreadMuted())
return VIEW_TYPE_MUTED_STATUS; return VIEW_TYPE_MUTED_STATUS;
return VIEW_TYPE_STATUS; return VIEW_TYPE_STATUS;
} }

@ -27,7 +27,7 @@ class CacheUpdater @Inject constructor(
is ReblogEvent -> is ReblogEvent ->
timelineDao.setReblogged(accountId, event.statusId, event.reblog) timelineDao.setReblogged(accountId, event.statusId, event.reblog)
is BookmarkEvent -> is BookmarkEvent ->
timelineDao.setBookmarked(accountId, event.statusId, event.bookmark ) timelineDao.setBookmarked(accountId, event.statusId, event.bookmark)
is UnfollowEvent -> is UnfollowEvent ->
timelineDao.removeAllByUser(accountId, event.accountId) timelineDao.removeAllByUser(accountId, event.accountId)
is StatusDeletedEvent -> is StatusDeletedEvent ->
@ -52,4 +52,4 @@ class CacheUpdater @Inject constructor(
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe() .subscribe()
} }
} }

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

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

@ -46,7 +46,8 @@ data class Status(
val poll: Poll?, val poll: Poll?,
val card: Card?, val card: Card?,
var content_type: String? = null, 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 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 return pleroma?.threadMuted ?: false
} }
fun setThreadMuted(mute: Boolean) {
if(pleroma?.threadMuted != null)
pleroma.threadMuted = mute
}
private fun getEditableText(): String { private fun getEditableText(): String {
val builder = SpannableStringBuilder(content) val builder = SpannableStringBuilder(content)
@ -158,7 +168,7 @@ data class Status(
} }
data class PleromaStatus( data class PleromaStatus(
@SerializedName("thread_muted") val threadMuted: Boolean? @SerializedName("thread_muted") var threadMuted: Boolean?
) )
data class Mention ( data class Mention (

@ -54,12 +54,7 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.keylesspalace.tusky.R; import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.adapter.NotificationsAdapter; import com.keylesspalace.tusky.adapter.NotificationsAdapter;
import com.keylesspalace.tusky.adapter.StatusBaseViewHolder; import com.keylesspalace.tusky.adapter.StatusBaseViewHolder;
import com.keylesspalace.tusky.appstore.BlockEvent; import com.keylesspalace.tusky.appstore.*;
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.db.AccountEntity; import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager; import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.di.Injectable; import com.keylesspalace.tusky.di.Injectable;
@ -329,6 +324,15 @@ public class NotificationsFragment extends SFragment implements
posAndNotification.second.getStatus(), posAndNotification.second.getStatus(),
event.getReblog()); 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 @Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) { public void onActivityCreated(@Nullable Bundle savedInstanceState) {
@ -381,6 +385,8 @@ public class NotificationsFragment extends SFragment implements
handleBookmarkEvent((BookmarkEvent) event); handleBookmarkEvent((BookmarkEvent) event);
} else if (event instanceof ReblogEvent) { } else if (event instanceof ReblogEvent) {
handleReblogEvent((ReblogEvent) event); handleReblogEvent((ReblogEvent) event);
} else if (event instanceof MuteStatusEvent) {
handleMuteStatusEvent((MuteStatusEvent) event);
} else if (event instanceof BlockEvent) { } else if (event instanceof BlockEvent) {
removeAllByAccountId(((BlockEvent) event).getAccountId()); removeAllByAccountId(((BlockEvent) event).getAccountId());
} else if (event instanceof PreferenceChangedEvent) { } else if (event instanceof PreferenceChangedEvent) {
@ -441,7 +447,7 @@ public class NotificationsFragment extends SFragment implements
notifications.setPairedItem(position, newViewData); notifications.setPairedItem(position, newViewData);
updateAdapter(); updateAdapter();
} }
@Override @Override
public void onFavourite(final boolean favourite, final int position) { public void onFavourite(final boolean favourite, final int position) {
final Notification notification = notifications.get(position).asRight(); final Notification notification = notifications.get(position).asRight();
@ -600,14 +606,30 @@ public class NotificationsFragment extends SFragment implements
(NotificationViewData.Concrete) notifications.getPairedItem(position); (NotificationViewData.Concrete) notifications.getPairedItem(position);
StatusViewData.Concrete statusViewData = StatusViewData.Concrete statusViewData =
new StatusViewData.Builder(old.getStatusViewData()) new StatusViewData.Builder(old.getStatusViewData())
.setMuted(isMuted) .setThreadMuted(isMuted)
.createStatusViewData(); .createStatusViewData();
Log.d("ASDASDASD", "position = " + position + " isMuted = " + isMuted);
NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(), NotificationViewData notificationViewData = new NotificationViewData.Concrete(old.getType(),
old.getId(), old.getAccount(), statusViewData, old.isExpanded()); old.getId(), old.getAccount(), statusViewData, old.isExpanded());
notifications.setPairedItem(position, notificationViewData); notifications.setPairedItem(position, notificationViewData);
updateAdapter(); 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 @Override
public void onLoadMore(int position) { public void onLoadMore(int position) {

@ -240,6 +240,20 @@ public abstract class SFragment extends BaseFragment implements Injectable {
replyToItem.setVisible(false); 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 -> { popup.setOnMenuItemClickListener(item -> {
switch (item.getItemId()) { switch (item.getItemId()) {
@ -298,6 +312,14 @@ public abstract class SFragment extends BaseFragment implements Injectable {
openReportPage(accountId, accountUsername, id); openReportPage(accountId, accountUsername, id);
return true; 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: { case R.id.status_unreblog_private: {
onReblog(false, position); onReblog(false, position);
return true; return true;

@ -206,7 +206,17 @@ interface MastodonApi {
fun unpinStatus( fun unpinStatus(
@Path("id") statusId: String @Path("id") statusId: String
): Single<Status> ): 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") @GET("api/v1/scheduled_statuses")
fun scheduledStatuses( fun scheduledStatuses(
@Query("limit") limit: Int? = null, @Query("limit") limit: Int? = null,

@ -27,6 +27,7 @@ import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
import java.lang.IllegalStateException import java.lang.IllegalStateException
import android.util.Log
/** /**
* Created by charlag on 3/24/18. * Created by charlag on 3/24/18.
@ -36,6 +37,7 @@ interface TimelineCases {
fun reblog(status: Status, reblog: Boolean): Single<Status> fun reblog(status: Status, reblog: Boolean): Single<Status>
fun favourite(status: Status, favourite: Boolean): Single<Status> fun favourite(status: Status, favourite: Boolean): Single<Status>
fun bookmark(status: Status, bookmark: Boolean): Single<Status> fun bookmark(status: Status, bookmark: Boolean): Single<Status>
fun muteStatus(status: Status, mute: Boolean)
fun mute(id: String) fun mute(id: String)
fun block(id: String) fun block(id: String)
fun delete(id: String): Single<DeletedStatus> fun delete(id: String): Single<DeletedStatus>
@ -103,6 +105,18 @@ class TimelineCasesImpl(
}) })
eventHub.dispatch(MuteEvent(id)) 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) { override fun block(id: String) {
val call = mastodonApi.blockAccount(id) val call = mastodonApi.blockAccount(id)
@ -143,4 +157,4 @@ class TimelineCasesImpl(
} }
} }
} }

@ -140,13 +140,13 @@ public class NotificationHelper {
} }
// Pleroma extension: don't notify about seen notifications // Pleroma extension: don't notify about seen notifications
if (body.getPleroma() != null && body.getPleroma().getSeen()) { if (body.getPleroma() != null && body.getPleroma().getSeen() == true) {
return; return;
} }
if (body.getStatus() != null if (body.getStatus() != null &&
&& body.getStatus().getPleroma() != null (body.getStatus().isUserMuted() == true ||
&& body.getStatus().getPleroma().getThreadMuted() == true) { body.getStatus().isThreadMuted() == true)) {
return; return;
} }

@ -65,7 +65,8 @@ public final class ViewDataUtils {
.setPoll(visibleStatus.getPoll()) .setPoll(visibleStatus.getPoll())
.setCard(visibleStatus.getCard()) .setCard(visibleStatus.getCard())
.setIsBot(visibleStatus.getAccount().getBot()) .setIsBot(visibleStatus.getAccount().getBot())
.setMuted(visibleStatus.isMuted()) .setUserMuted(visibleStatus.isUserMuted())
.setThreadMuted(visibleStatus.isThreadMuted())
.createStatusViewData(); .createStatusViewData();
} }

@ -91,7 +91,8 @@ public abstract class StatusViewData {
@Nullable @Nullable
private final PollViewData poll; private final PollViewData poll;
private final boolean isBot; 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, public Concrete(String id, Spanned content, boolean reblogged, boolean favourited, boolean bookmarked,
@Nullable String spoilerText, Status.Visibility visibility, List<Attachment> attachments, @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, Date createdAt, int reblogsCount, int favouritesCount, @Nullable String inReplyToId,
@Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled, @Nullable Status.Mention[] mentions, String senderId, boolean rebloggingEnabled,
Status.Application application, List<Emoji> statusEmojis, List<Emoji> accountEmojis, @Nullable Card card, 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; this.id = id;
if (Build.VERSION.SDK_INT == 23) { if (Build.VERSION.SDK_INT == 23) {
@ -140,7 +142,8 @@ public abstract class StatusViewData {
this.isCollapsed = isCollapsed; this.isCollapsed = isCollapsed;
this.poll = poll; this.poll = poll;
this.isBot = isBot; this.isBot = isBot;
this.isMuted = isMuted; this.isThreadMuted = isThreadMuted;
this.isUserMuted = isUserMuted;
} }
public String getId() { public String getId() {
@ -289,8 +292,12 @@ public abstract class StatusViewData {
return id.hashCode(); return id.hashCode();
} }
public boolean isMuted() { public boolean isThreadMuted() {
return isMuted; return isThreadMuted;
}
public boolean isUserMuted() {
return isUserMuted;
} }
public boolean deepEquals(StatusViewData o) { public boolean deepEquals(StatusViewData o) {
@ -327,7 +334,8 @@ public abstract class StatusViewData {
Objects.equals(card, concrete.card) && Objects.equals(card, concrete.card) &&
Objects.equals(poll, concrete.poll) && Objects.equals(poll, concrete.poll) &&
isCollapsed == concrete.isCollapsed && isCollapsed == concrete.isCollapsed &&
isMuted == concrete.isMuted; isThreadMuted == concrete.isThreadMuted &&
isUserMuted == concrete.isUserMuted;
} }
static Spanned replaceCrashingCharacters(Spanned content) { 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 boolean isCollapsed; /** Whether the status is shown partially or fully */
private PollViewData poll; private PollViewData poll;
private boolean isBot; private boolean isBot;
private boolean isMuted; private boolean isThreadMuted;
private boolean isUserMuted;
public Builder() { public Builder() {
} }
@ -471,7 +480,8 @@ public abstract class StatusViewData {
isCollapsed = viewData.isCollapsed(); isCollapsed = viewData.isCollapsed();
poll = viewData.poll; poll = viewData.poll;
isBot = viewData.isBot(); isBot = viewData.isBot();
isMuted = viewData.isMuted; isThreadMuted = viewData.isThreadMuted;
isUserMuted = viewData.isUserMuted;
} }
public Builder setId(String id) { public Builder setId(String id) {
@ -643,8 +653,13 @@ public abstract class StatusViewData {
return this; return this;
} }
public Builder setMuted(Boolean isMuted) { public Builder setUserMuted(Boolean isUserMuted) {
this.isMuted = isMuted; this.isUserMuted = isUserMuted;
return this;
}
public Builder setThreadMuted(Boolean isThreadMuted) {
this.isThreadMuted = isThreadMuted;
return this; return this;
} }
@ -657,7 +672,7 @@ public abstract class StatusViewData {
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,
statusEmojis, accountEmojis, card, isCollapsible, isCollapsed, poll, isBot, isMuted); statusEmojis, accountEmojis, card, isCollapsible, isCollapsed, poll, isBot, isThreadMuted, isUserMuted);
} }
} }
} }

@ -21,6 +21,14 @@
<item <item
android:id="@+id/status_open_as" android:id="@+id/status_open_as"
android:title="@string/action_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 <item
android:id="@+id/status_download_media" android:id="@+id/status_download_media"
android:title="@string/download_media" /> android:title="@string/download_media" />

@ -21,6 +21,14 @@
<item <item
android:id="@+id/status_open_as" android:id="@+id/status_open_as"
android:title="@string/action_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 <item
android:id="@+id/status_reblog_private" android:id="@+id/status_reblog_private"
android:title="@string/reblog_private" android:title="@string/reblog_private"

@ -2,7 +2,8 @@
<string name="action_reply_to">Reply to</string> <string name="action_reply_to">Reply to</string>
<string name="action_markdown">Markdown</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_appname">Application name</string>
<string name="hint_website">Application website</string> <string name="hint_website">Application website</string>

Loading…
Cancel
Save