Implement proper hiding muted users and conversations

main
Alibek Omarov 4 years ago
parent ed16f1b2bc
commit ed7bfa182b
  1. 2
      app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt
  2. 1
      app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationFetcher.kt
  3. 2
      app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt
  4. 7
      app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt
  5. 2
      app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt
  6. 11
      app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesDataSource.kt
  7. 32
      app/src/main/java/com/keylesspalace/tusky/fragment/AccountMediaFragment.kt
  8. 82
      app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
  9. 27
      app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.java
  10. 96
      app/src/main/java/com/keylesspalace/tusky/fragment/TimelineFragment.java
  11. 42
      app/src/main/java/com/keylesspalace/tusky/fragment/ViewThreadFragment.java
  12. 48
      app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
  13. 5
      app/src/main/java/com/keylesspalace/tusky/network/TimelineCases.kt
  14. 3
      app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt
  15. 1
      app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt
  16. 5
      app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountViewModel.kt

@ -13,7 +13,7 @@ data class MuteStatusEvent(val statusId: String, val mute: Boolean) : Dispatchab
data class EmojiReactEvent(val newStatus: Status) : Dispatchable
data class UnfollowEvent(val accountId: String) : Dispatchable
data class BlockEvent(val accountId: String) : Dispatchable
data class MuteEvent(val accountId: String) : Dispatchable
data class MuteEvent(val accountId: String, val mute: Boolean) : Dispatchable
data class StatusDeletedEvent(val statusId: String) : Dispatchable
data class StatusPreviewEvent(val status: Status) : Dispatchable
data class StatusComposedEvent(val status: Status) : Dispatchable

@ -42,7 +42,6 @@ class NotificationFetcher @Inject constructor(
authHeader,
account.domain,
account.lastNotificationId,
true,
Notification.Type.asStringList
).blockingGet()

@ -132,7 +132,7 @@ class PreferencesActivity : BaseActivity(), SharedPreferences.OnSharedPreference
}
"statusTextSize", "absoluteTimeView", "showBotOverlay", "animateGifAvatars",
"useBlurhash", "showCardsInTimelines", "confirmReblogs", "hideMutedUsers",
"useBlurhash", "showCardsInTimelines", "confirmReblogs",
"enableSwipeForTabs", "bigEmojis", "mainNavPosition", PrefKeys.HIDE_TOP_TOOLBAR -> {
restartActivitiesOnExit = true
}

@ -165,6 +165,13 @@ class PreferencesFragment : PreferenceFragmentCompat(), Injectable {
isSingleLineTitle = false
}
switchPreference {
setDefaultValue(false)
key = PrefKeys.HIDE_MUTED_USERS
setTitle(R.string.pref_title_hide_muted_users)
isSingleLineTitle = true
}
switchPreference {
setDefaultValue(true)
key = PrefKeys.ENABLE_SWIPE_FOR_TABS

@ -140,7 +140,7 @@ class ReportViewModel @Inject constructor(
val muting = relationship?.muting == true
muteStateMutable.value = Success(muting)
if (muting) {
eventHub.dispatch(MuteEvent(accountId))
eventHub.dispatch(MuteEvent(accountId, true))
}
},
{ error ->

@ -73,12 +73,11 @@ class StatusesDataSource(private val accountId: String,
retryInitial = null
initialLoad.postValue(NetworkState.LOADING)
val initialKey = params.requestedInitialKey
val withMuted = true // TODO: configurable
if (initialKey == null) {
mastodonApi.accountStatusesObservable(accountId, null, null, params.requestedLoadSize, true, withMuted)
mastodonApi.accountStatusesObservable(accountId, null, null, params.requestedLoadSize, true)
} else {
mastodonApi.statusObservable(initialKey).zipWith(
mastodonApi.accountStatusesObservable(accountId, params.requestedInitialKey, null, params.requestedLoadSize - 1, true, withMuted),
mastodonApi.accountStatusesObservable(accountId, params.requestedInitialKey, null, params.requestedLoadSize - 1, true),
BiFunction { status: Status, list: List<Status> ->
val ret = ArrayList<Status>()
ret.add(status)
@ -107,8 +106,7 @@ class StatusesDataSource(private val accountId: String,
override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<Status>) {
networkStateAfter.postValue(NetworkState.LOADING)
retryAfter = null
val withMuted = true // TODO: configurable
mastodonApi.accountStatusesObservable(accountId, params.key, null, params.requestedLoadSize, true, withMuted)
mastodonApi.accountStatusesObservable(accountId, params.key, null, params.requestedLoadSize, true)
.doOnSubscribe {
disposables.add(it)
}
@ -130,8 +128,7 @@ class StatusesDataSource(private val accountId: String,
override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<Status>) {
networkStateBefore.postValue(NetworkState.LOADING)
retryBefore = null
val withMuted = true // TODO: configurable
mastodonApi.accountStatusesObservable(accountId, null, params.key, params.requestedLoadSize, true, withMuted)
mastodonApi.accountStatusesObservable(accountId, null, params.key, params.requestedLoadSize, true)
.doOnSubscribe {
disposables.add(it)
}

@ -24,6 +24,7 @@ import android.view.ViewGroup
import android.widget.ImageView
import androidx.core.app.ActivityOptionsCompat
import androidx.core.view.ViewCompat
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
@ -34,6 +35,7 @@ import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.interfaces.RefreshableFragment
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.settings.PrefKeys
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.util.hide
import com.keylesspalace.tusky.util.show
@ -72,6 +74,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
private var isSwipeToRefreshEnabled: Boolean = true
private var needToRefresh = false
private var filterMuted = false
@Inject
lateinit var api: MastodonApi
@ -115,10 +118,12 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
val body = response.body()
body?.let { fetched ->
statuses.addAll(0, fetched)
// filter muted statuses if needed
val filtered = fetched.filter { !(filterMuted && it.muted) }
statuses.addAll(0, filtered)
// flatMap requires iterable but I don't want to box each array into list
val result = mutableListOf<AttachmentViewData>()
for (status in fetched) {
for (status in filtered) {
result.addAll(AttachmentViewData.list(status))
}
adapter.addTop(result)
@ -148,11 +153,15 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
body?.let { fetched ->
Log.d(TAG, "fetched ${fetched.size} statuses")
if (fetched.isNotEmpty()) Log.d(TAG, "first: ${fetched.first().id}, last: ${fetched.last().id}")
statuses.addAll(fetched)
// filter muted statuses if needed
val filtered = fetched.filter { !(filterMuted && it.muted) }
statuses.addAll(filtered)
Log.d(TAG, "now there are ${statuses.size} statuses")
// flatMap requires iterable but I don't want to box each array into list
val result = mutableListOf<AttachmentViewData>()
for (status in fetched) {
for (status in filtered) {
result.addAll(AttachmentViewData.list(status))
}
adapter.addBottom(result)
@ -201,8 +210,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
statuses.lastOrNull()?.let { last ->
Log.d(TAG, "Requesting statuses with max_id: ${last.id}, (bottom)")
fetchingStatus = FetchingStatus.FETCHING_BOTTOM
val withMuted = true // TODO: configurable
currentCall = api.accountStatuses(accountId, last.id, null, null, null, true, null, withMuted)
currentCall = api.accountStatuses(accountId, last.id, null, null, null, true, null)
currentCall?.enqueue(bottomCallback)
}
}
@ -210,19 +218,22 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
}
})
filterMuted = PreferenceManager.getDefaultSharedPreferences(requireContext()).getBoolean(
PrefKeys.HIDE_MUTED_USERS, false
)
doInitialLoadingIfNeeded()
}
private fun refresh() {
statusView.hide()
val withMuted = true // TODO: configurable
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return
currentCall = if (statuses.isEmpty()) {
fetchingStatus = FetchingStatus.INITIAL_FETCHING
api.accountStatuses(accountId, null, null, null, null, true, null, withMuted)
api.accountStatuses(accountId, null, null, null, null, true, null)
} else {
fetchingStatus = FetchingStatus.REFRESHING
api.accountStatuses(accountId, null, statuses[0].id, null, null, true, null, withMuted)
api.accountStatuses(accountId, null, statuses[0].id, null, null, true, null)
}
currentCall?.enqueue(callback)
@ -236,8 +247,7 @@ class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
}
if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) {
fetchingStatus = FetchingStatus.INITIAL_FETCHING
val withMuted = true // TODO: configurable
currentCall = api.accountStatuses(accountId, null, null, null, null, true, null, withMuted)
currentCall = api.accountStatuses(accountId, null, null, null, null, true, null)
currentCall?.enqueue(callback)
}
else if (needToRefresh)

@ -67,6 +67,7 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.interfaces.ReselectableFragment;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.HttpHeaderLink;
@ -168,6 +169,7 @@ public class NotificationsFragment extends SFragment implements
private boolean alwaysOpenSpoiler;
private boolean showNotificationsFilter;
private boolean showingError;
private boolean withMuted;
// Each element is either a Notification for loading data or a Placeholder
private final PairedList<Either<Placeholder, Notification>, NotificationViewData> notifications
@ -201,7 +203,7 @@ public class NotificationsFragment extends SFragment implements
View rootView = inflater.inflate(R.layout.fragment_timeline_notifications, container, false);
@NonNull Context context = inflater.getContext(); // from inflater to silence warning
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
boolean showNotificationsFilterSetting = preferences.getBoolean("showNotificationsFilter", true);
//Clear notifications on filter visibility change to force refresh
@ -247,6 +249,7 @@ public class NotificationsFragment extends SFragment implements
CardViewMode.NONE,
preferences.getBoolean("confirmReblogs", true)
);
withMuted = !preferences.getBoolean(PrefKeys.HIDE_MUTED_USERS, false);
adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(),
dataSource, statusDisplayOptions, this, this, this);
@ -340,20 +343,48 @@ public class NotificationsFragment extends SFragment implements
int conversationId = posAndNotification.second.getStatus().getConversationId();
if(conversationId == -1) { // invalid conversation ID
setMutedStatusForStatus(posAndNotification.first, posAndNotification.second.getStatus(), event.getMute());
if(withMuted) {
setMutedStatusForStatus(posAndNotification.first, posAndNotification.second.getStatus(), event.getMute(), event.getMute());
} else {
notifications.remove(posAndNotification.first);
}
} else {
//noinspection ConstantConditions
// using iterator to safely remove items while iterating
if(withMuted) {
for (int i = 0; i < notifications.size(); i++) {
Notification notification = notifications.get(i).asRightOrNull();
if (notification != null && notification.getStatus() != null
&& notification.getType() == Notification.Type.MENTION &&
notification.getStatus().getConversationId() == conversationId) {
setMutedStatusForStatus(i, notification.getStatus(), event.getMute(), event.getMute());
}
}
} else {
removeAllByConversationId(conversationId);
}
}
updateAdapter();
}
private void handleMuteEvent(MuteEvent event) {
String id = event.getAccountId();
boolean mute = event.getMute();
if(withMuted) {
for (int i = 0; i < notifications.size(); i++) {
Notification notification = notifications.get(i).asRightOrNull();
if (notification != null && notification.getStatus() != null
&& notification.getType() == Notification.Type.MENTION &&
notification.getStatus().getConversationId() == conversationId) {
setMutedStatusForStatus(i, notification.getStatus(), event.getMute());
if (notification != null
&& notification.getStatus() != null
&& notification.getType() == Notification.Type.MENTION
&& notification.getAccount().getId().equals(id)
&& !notification.getStatus().isThreadMuted()) {
setMutedStatusForStatus(i, notification.getStatus(), mute, false);
}
}
updateAdapter();
} else {
removeAllByAccountId(id);
}
updateAdapter();
}
@Override
@ -411,6 +442,8 @@ public class NotificationsFragment extends SFragment implements
handleMuteStatusEvent((MuteStatusEvent) event);
} else if (event instanceof BlockEvent) {
removeAllByAccountId(((BlockEvent) event).getAccountId());
} else if (event instanceof MuteEvent) {
handleMuteEvent((MuteEvent)event);
} else if (event instanceof PreferenceChangedEvent) {
onPreferenceChanged(((PreferenceChangedEvent) event).getPreferenceKey());
} else if (event instanceof EmojiReactEvent) {
@ -645,13 +678,13 @@ public class NotificationsFragment extends SFragment implements
updateAdapter();
}
private void setMutedStatusForStatus(int position, Status status, boolean muted) {
status.setThreadMuted(muted);
private void setMutedStatusForStatus(int position, Status status, boolean muted, boolean threadMuted) {
status.setThreadMuted(threadMuted);
NotificationViewData.Concrete viewdata = (NotificationViewData.Concrete) notifications.getPairedItem(position);
StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder(viewdata.getStatusViewData());
viewDataBuilder.setThreadMuted(muted);
viewDataBuilder.setThreadMuted(threadMuted);
viewDataBuilder.setMuted(muted);
NotificationViewData.Concrete newViewData = new NotificationViewData.Concrete(
@ -924,9 +957,11 @@ public class NotificationsFragment extends SFragment implements
}
private void onPreferenceChanged(String key) {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(getContext());
switch (key) {
case "fabHide": {
hideFab = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("fabHide", false);
hideFab = sharedPreferences.getBoolean("fabHide", false);
break;
}
case "mediaPreviewEnabled": {
@ -938,11 +973,15 @@ public class NotificationsFragment extends SFragment implements
}
case "showNotificationsFilter": {
if (isAdded()) {
showNotificationsFilter = PreferenceManager.getDefaultSharedPreferences(getContext()).getBoolean("showNotificationsFilter", true);
showNotificationsFilter = sharedPreferences.getBoolean("showNotificationsFilter", true);
updateFilterVisibility();
fullyRefreshWithProgressBar(true);
}
}
case PrefKeys.HIDE_MUTED_USERS: {
withMuted = !sharedPreferences.getBoolean(PrefKeys.HIDE_MUTED_USERS, false);
fullyRefresh();
}
}
}
@ -952,6 +991,21 @@ public class NotificationsFragment extends SFragment implements
updateAdapter();
}
private void removeAllByConversationId(int conversationId) {
// using iterator to safely remove items while iterating
Iterator<Either<Placeholder, Notification>> iterator = notifications.iterator();
while (iterator.hasNext()) {
Either<Placeholder, Notification> placeholderOrNotification = iterator.next();
Notification notification = placeholderOrNotification.asRightOrNull();
if (notification != null && notification.getStatus() != null
&& notification.getType() == Notification.Type.MENTION &&
notification.getStatus().getConversationId() == conversationId) {
iterator.remove();
}
}
updateAdapter();
}
private void removeAllByAccountId(String accountId) {
// using iterator to safely remove items while iterating
Iterator<Either<Placeholder, Notification>> iterator = notifications.iterator();
@ -1020,8 +1074,6 @@ public class NotificationsFragment extends SFragment implements
bottomLoading = true;
}
boolean withMuted = true; // TODO: configurable
Call<List<Notification>> call = mastodonApi.notifications(fromId, uptoId, LOAD_AT_ONCE, showNotificationsFilter ? notificationFilter : null, withMuted);
call.enqueue(new Callback<List<Notification>>() {

@ -21,6 +21,7 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Environment;
@ -62,6 +63,7 @@ import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.entity.EmojiReaction;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.network.TimelineCases;
import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.util.LinkHelper;
import com.keylesspalace.tusky.viewdata.AttachmentViewData;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
@ -101,6 +103,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
private boolean filterRemoveRegex;
private Matcher filterRemoveRegexMatcher;
private static Matcher alphanumeric = Pattern.compile("^\\w+$").matcher("");
private boolean filterMuted;
@Inject
public MastodonApi mastodonApi;
@ -543,8 +546,23 @@ public abstract class SFragment extends BaseFragment implements Injectable {
});
}
@VisibleForTesting
public void reloadFilters(boolean forceRefresh) {
public boolean isFilteringMuted() {
return filterMuted;
}
public void updateMuteFilter(@NonNull SharedPreferences pref, boolean reload) {
filterMuted = pref.getBoolean(PrefKeys.HIDE_MUTED_USERS, false);
if(reload) {
refreshAfterApplyingFilters();
}
}
public void reloadFilters(SharedPreferences pref, boolean forceRefresh) {
if(pref != null) {
updateMuteFilter(pref, false); // will be reloaded later
}
if (filters != null && !forceRefresh) {
applyFilters(forceRefresh);
return;
@ -581,6 +599,9 @@ public abstract class SFragment extends BaseFragment implements Injectable {
@VisibleForTesting
public boolean shouldFilterStatus(Status status) {
if (filterMuted && status.getMuted()) {
return true;
}
if (filterRemoveRegex && status.getPoll() != null) {
for (PollOption option : status.getPoll().getOptions()) {
@ -594,7 +615,7 @@ public abstract class SFragment extends BaseFragment implements Injectable {
|| (!status.getSpoilerText().isEmpty() && filterRemoveRegexMatcher.reset(status.getActionableStatus().getSpoilerText()).find())));
}
private void applyFilters(boolean refresh) {
public void applyFilters(boolean refresh) {
List<String> tokens = new ArrayList<>();
for (Filter filter : filters) {
if (filterIsRelevant(filter)) {

@ -61,6 +61,7 @@ import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.repository.Placeholder;
import com.keylesspalace.tusky.repository.TimelineRepository;
import com.keylesspalace.tusky.repository.TimelineRequestMode;
import com.keylesspalace.tusky.settings.PrefKeys;
import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.Either;
import com.keylesspalace.tusky.util.HttpHeaderLink;
@ -245,7 +246,6 @@ public class TimelineFragment extends SFragment implements
adapter = new TimelineAdapter(dataSource, statusDisplayOptions, this);
isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true);
}
@Override
@ -288,7 +288,7 @@ public class TimelineFragment extends SFragment implements
private void tryCache() {
// Request timeline from disk to make it quick, then replace it with timeline from
// the server to update it
this.timelineRepo.getStatuses(null, null, null, LOAD_AT_ONCE,
timelineRepo.getStatuses(null, null, null, LOAD_AT_ONCE,
TimelineRequestMode.DISK)
.observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
@ -373,7 +373,8 @@ public class TimelineFragment extends SFragment implements
filter = preferences.getBoolean("tabFilterHomeBoosts", true);
filterRemoveReblogs = kind == Kind.HOME && !filter;
reloadFilters(false);
reloadFilters(preferences,false);
}
private static boolean filterContextMatchesKind(Kind kind, List<String> filterContext) {
@ -455,12 +456,13 @@ public class TimelineFragment extends SFragment implements
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext());
/* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't
* guaranteed to be set until then. */
if (actionButtonPresent()) {
/* Use a modified scroll listener that both loads more statuses as it goes, and hides
* the follow button on down-scroll. */
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext());
hideFab = preferences.getBoolean("fabHide", false);
scrollListener = new EndlessOnScrollListener(layoutManager) {
@Override
@ -530,8 +532,7 @@ public class TimelineFragment extends SFragment implements
}
} else if (event instanceof MuteEvent) {
if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) {
String id = ((MuteEvent) event).getAccountId();
removeAllByAccountId(id);
handleMuteEvent((MuteEvent)event);
}
} else if (event instanceof DomainMuteEvent) {
if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) {
@ -700,12 +701,12 @@ public class TimelineFragment extends SFragment implements
updateAdapter();
}
private void setMutedStatusForStatus(int position, Status status, boolean muted) {
status.setThreadMuted(muted);
private void setMutedStatusForStatus(int position, Status status, boolean muted, boolean threadMuted) {
status.setThreadMuted(threadMuted);
StatusViewData.Builder statusViewData = new StatusViewData.Builder((StatusViewData.Concrete)statuses.getPairedItem(position));
statusViewData.setMuted(muted);
statusViewData.setThreadMuted(muted);
statusViewData.setThreadMuted(threadMuted);
statuses.setPairedItem(position, statusViewData.createStatusViewData());
}
@ -912,13 +913,17 @@ public class TimelineFragment extends SFragment implements
}
break;
}
case PrefKeys.HIDE_MUTED_USERS: {
updateMuteFilter(sharedPreferences, true);
break;
}
case Filter.HOME:
case Filter.NOTIFICATIONS:
case Filter.THREAD:
case Filter.PUBLIC:
case Filter.ACCOUNT: {
if (filterContextMatchesKind(kind, Collections.singletonList(key))) {
reloadFilters(true);
reloadFilters(sharedPreferences, true);
}
break;
}
@ -936,6 +941,19 @@ public class TimelineFragment extends SFragment implements
updateAdapter();
}
private void removeAllByConversationId(int conversationId) {
// using iterator to safely remove items while iterating
Iterator<Either<Placeholder, Status>> iterator = statuses.iterator();
while (iterator.hasNext()) {
Status status = iterator.next().asRightOrNull();
if (status != null &&
(status.getConversationId() == conversationId) || status.getActionableStatus().getConversationId() == conversationId) {
iterator.remove();
}
}
updateAdapter();
}
private void removeAllByAccountId(String accountId) {
// using iterator to safely remove items while iterating
Iterator<Either<Placeholder, Status>> iterator = statuses.iterator();
@ -1026,32 +1044,30 @@ public class TimelineFragment extends SFragment implements
private Call<List<Status>> getFetchCallByTimelineType(String fromId, String uptoId) {
MastodonApi api = mastodonApi;
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getActivity());
boolean withMuted = !preferences.getBoolean("hideMutedUsers", false);
switch (kind) {
default:
case HOME:
return api.homeTimeline(fromId, uptoId, LOAD_AT_ONCE, withMuted);
return api.homeTimeline(fromId, uptoId, LOAD_AT_ONCE);
case PUBLIC_FEDERATED:
return api.publicTimeline(null, fromId, uptoId, LOAD_AT_ONCE, withMuted);
return api.publicTimeline(null, fromId, uptoId, LOAD_AT_ONCE);
case PUBLIC_LOCAL:
return api.publicTimeline(true, fromId, uptoId, LOAD_AT_ONCE, withMuted);
return api.publicTimeline(true, fromId, uptoId, LOAD_AT_ONCE);
case TAG:
String firstHashtag = tags.get(0);
List<String> additionalHashtags = tags.subList(1, tags.size());
return api.hashtagTimeline(firstHashtag, additionalHashtags, null, fromId, uptoId, LOAD_AT_ONCE, withMuted);
return api.hashtagTimeline(firstHashtag, additionalHashtags, null, fromId, uptoId, LOAD_AT_ONCE);
case USER:
return api.accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, true, null, null, withMuted);
return api.accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, true, null, null);
case USER_PINNED:
return api.accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, null, null, true, withMuted);
return api.accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, null, null, true);
case USER_WITH_REPLIES:
return api.accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, null, null, null, withMuted);
return api.accountStatuses(id, fromId, uptoId, LOAD_AT_ONCE, null, null, null);
case FAVOURITES:
return api.favourites(fromId, uptoId, LOAD_AT_ONCE, withMuted);
return api.favourites(fromId, uptoId, LOAD_AT_ONCE);
case BOOKMARKS:
return api.bookmarks(fromId, uptoId, LOAD_AT_ONCE, withMuted);
return api.bookmarks(fromId, uptoId, LOAD_AT_ONCE);
case LIST:
return api.listTimeline(id, fromId, uptoId, LOAD_AT_ONCE, withMuted);
return api.listTimeline(id, fromId, uptoId, LOAD_AT_ONCE);
}
}
@ -1421,17 +1437,45 @@ public class TimelineFragment extends SFragment implements
int conversationId = eventStatus.getConversationId();
if(conversationId == -1) { // invalid conversation ID
setMutedStatusForStatus(pos, eventStatus, event.getMute());
if(isFilteringMuted()) {
statuses.remove(pos);
} else {
setMutedStatusForStatus(pos, eventStatus, event.getMute(), event.getMute());
}
updateAdapter();
} else {
//noinspection ConstantConditions
if(isFilteringMuted()) {
removeAllByConversationId(conversationId);
} else {
for (int i = 0; i < statuses.size(); i++) {
Status status = statuses.get(i).asRightOrNull();
if (status != null && status.getConversationId() == conversationId) {
setMutedStatusForStatus(i, status, event.getMute(), event.getMute());
}
}
updateAdapter();
}
}
}
private void handleMuteEvent(MuteEvent event) {
String id = event.getAccountId();
boolean muting = event.getMute();
if(isFilteringMuted() && muting) {
removeAllByAccountId(id);
} else {
for (int i = 0; i < statuses.size(); i++) {
Status status = statuses.get(i).asRightOrNull();
if (status != null && status.getConversationId() == conversationId) {
setMutedStatusForStatus(i, status, event.getMute());
if (status != null
&& status.getAccount().getId().equals(id)
&& !status.isThreadMuted()) {
setMutedStatusForStatus(i, status, muting, false);
}
}
updateAdapter();
}
updateAdapter();
}
private List<Either<Placeholder, Status>> liftStatusList(List<Status> list) {

@ -53,7 +53,6 @@ import com.keylesspalace.tusky.util.CardViewMode;
import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate;
import com.keylesspalace.tusky.util.PairedList;
import com.keylesspalace.tusky.util.StatusDisplayOptions;
import com.keylesspalace.tusky.util.ThemeUtils;
import com.keylesspalace.tusky.util.ViewDataUtils;
import com.keylesspalace.tusky.view.ConversationLineItemDecoration;
import com.keylesspalace.tusky.viewdata.StatusViewData;
@ -155,7 +154,7 @@ public final class ViewThreadFragment extends SFragment implements
recyclerView.addItemDecoration(new ConversationLineItemDecoration(context));
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
reloadFilters(false);
reloadFilters(PreferenceManager.getDefaultSharedPreferences(context), false);
recyclerView.setAdapter(adapter);
@ -184,6 +183,8 @@ public final class ViewThreadFragment extends SFragment implements
handleBookmarkEvent((BookmarkEvent) event);
} else if (event instanceof BlockEvent) {
removeAllByAccountId(((BlockEvent) event).getAccountId());
} else if (event instanceof MuteEvent) {
handleMuteEvent((MuteEvent) event);
} else if (event instanceof StatusComposedEvent) {
handleStatusComposedEvent((StatusComposedEvent) event);
} else if (event instanceof StatusDeletedEvent) {
@ -207,7 +208,7 @@ public final class ViewThreadFragment extends SFragment implements
.createStatusViewData();
statuses.setPairedItem(i, newViewData);
}
adapter.setStatuses(statuses.getPairedCopy());
updateAdapter();
updateRevealIcon();
}
@ -411,7 +412,7 @@ public final class ViewThreadFragment extends SFragment implements
getActivity().finish();
}
statuses.remove(position);
adapter.setStatuses(statuses.getPairedCopy());
updateAdapter();
}
public void onVoteInPoll(int position, @NonNull List<Integer> choices) {
@ -441,6 +442,10 @@ public final class ViewThreadFragment extends SFragment implements
adapter.setItem(position, newViewData, true);
}
private void updateAdapter() {
adapter.setStatuses(statuses.getPairedCopy());
}
private void removeAllByAccountId(String accountId) {
Status status = null;
if (!statuses.isEmpty()) {
@ -461,7 +466,7 @@ public final class ViewThreadFragment extends SFragment implements
return;
}
adapter.setDetailedStatusPosition(statusIndex);
adapter.setStatuses(statuses.getPairedCopy());
updateAdapter();
}
private void sendStatusRequest(final String id) {
@ -602,6 +607,33 @@ public final class ViewThreadFragment extends SFragment implements
updateRevealIcon();
}
private void setMutedStatusForStatus(int position, Status status, boolean muted) {
StatusViewData.Builder statusViewData = new StatusViewData.Builder(statuses.getPairedItem(position));
statusViewData.setMuted(muted);
statuses.setPairedItem(position, statusViewData.createStatusViewData());
}
private void handleMuteEvent(MuteEvent event) {
String id = event.getAccountId();
boolean muting = event.getMute();
if(isFilteringMuted()) {
removeAllByAccountId(id);
} else {
for (int i = 0; i < statuses.size(); i++) {
Status status = statuses.get(i);
if (status != null
&& status.getAccount().getId().equals(id)
&& !status.isThreadMuted()) {
setMutedStatusForStatus(i, status, muting);
}
}
updateAdapter();
}
}
private void handleFavEvent(FavoriteEvent event) {
Pair<Integer, Status> posAndStatus = findStatusAndPos(event.getStatusId());
if (posAndStatus == null) return;

@ -51,49 +51,44 @@ interface MastodonApi {
@GET("api/v1/filters")
fun getFilters(): Call<List<Filter>>
@GET("api/v1/timelines/home")
@GET("api/v1/timelines/home?with_muted=true")
fun homeTimeline(
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?,
@Query("with_muted") withMuted: Boolean?
@Query("limit") limit: Int?
): Call<List<Status>>
@GET("api/v1/timelines/home")
@GET("api/v1/timelines/home?with_muted=true")
fun homeTimelineSingle(
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?,
@Query("with_muted") withMuted: Boolean?
@Query("limit") limit: Int?
): Single<List<Status>>
@GET("api/v1/timelines/public")
@GET("api/v1/timelines/public?with_muted=true")
fun publicTimeline(
@Query("local") local: Boolean?,
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?,
@Query("with_muted") withMuted: Boolean?
@Query("limit") limit: Int?
): Call<List<Status>>
@GET("api/v1/timelines/tag/{hashtag}")
@GET("api/v1/timelines/tag/{hashtag}?with_muted=true")
fun hashtagTimeline(
@Path("hashtag") hashtag: String,
@Query("any[]") any: List<String>?,
@Query("local") local: Boolean?,
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?,
@Query("with_muted") withMuted: Boolean?
@Query("limit") limit: Int?
): Call<List<Status>>
@GET("api/v1/timelines/list/{listId}")
@GET("api/v1/timelines/list/{listId}?with_muted=true")
fun listTimeline(
@Path("listId") listId: String,
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?,
@Query("with_muted") withMuted: Boolean?
@Query("limit") limit: Int?
): Call<List<Status>>
@GET("api/v1/notifications")
@ -112,12 +107,11 @@ interface MastodonApi {
@Query("timeline[]") timelines: List<String>
): Single<Map<String, Marker>>
@GET("api/v1/notifications")
@GET("api/v1/notifications?with_muted=true")
fun notificationsWithAuth(
@Header("Authorization") auth: String,
@Header(DOMAIN_HEADER) domain: String,
@Query("since_id") sinceId: String?,
@Query("with_muted") withMuted: Boolean?,
@Query("include_types[]") includeTypes: List<String>?
): Single<List<Notification>>
@ -288,7 +282,7 @@ interface MastodonApi {
* @param excludeReplies only return statuses that are no replies
* @param onlyMedia only return statuses that have media attached
*/
@GET("api/v1/accounts/{id}/statuses")
@GET("api/v1/accounts/{id}/statuses?with_muted=true")
fun accountStatuses(
@Path("id") accountId: String,
@Query("max_id") maxId: String?,
@ -296,8 +290,7 @@ interface MastodonApi {
@Query("limit") limit: Int?,
@Query("exclude_replies") excludeReplies: Boolean?,
@Query("only_media") onlyMedia: Boolean?,
@Query("pinned") pinned: Boolean?,
@Query("with_muted") withMuted: Boolean?
@Query("pinned") pinned: Boolean?
): Call<List<Status>>
@GET("api/v1/accounts/{id}/followers")
@ -392,20 +385,18 @@ interface MastodonApi {
@HTTP(method = "DELETE", path = "api/v1/domain_blocks", hasBody = true)
fun unblockDomain(@Field("domain") domain: String): Call<Any>
@GET("api/v1/favourites")
@GET("api/v1/favourites?with_muted=true")
fun favourites(
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?,
@Query("with_muted") withMuted: Boolean?
@Query("limit") limit: Int?
): Call<List<Status>>
@GET("api/v1/bookmarks")
@GET("api/v1/bookmarks?with_muted=true")
fun bookmarks(
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?,
@Query("with_muted") withMuted: Boolean?
@Query("limit") limit: Int?
): Call<List<Status>>
@GET("api/v1/follow_requests")
@ -572,14 +563,13 @@ interface MastodonApi {
@Field("forward") isNotifyRemote: Boolean?
): Single<ResponseBody>
@GET("api/v1/accounts/{id}/statuses")
@GET("api/v1/accounts/{id}/statuses?with_muted=true")
fun accountStatusesObservable(
@Path("id") accountId: String,
@Query("max_id") maxId: String?,
@Query("since_id") sinceId: String?,
@Query("limit") limit: Int?,
@Query("exclude_reblogs") excludeReblogs: Boolean?,
@Query("with_muted") withMuted: Boolean?
@Query("exclude_reblogs") excludeReblogs: Boolean?
): Single<List<Status>>
@GET("api/v1/statuses/{id}")

@ -103,7 +103,7 @@ class TimelineCasesImpl(
override fun onFailure(call: Call<Relationship>, t: Throwable) {}
})
eventHub.dispatch(MuteEvent(id))
eventHub.dispatch(MuteEvent(id, true))
}
override fun muteStatus(status: Status, mute: Boolean) {
@ -115,7 +115,7 @@ class TimelineCasesImpl(
mastodonApi.unmuteStatus(id)
}).subscribe( { status ->
eventHub.dispatch(MuteStatusEvent(status.id, mute))
}, {})
}, {}).addTo(this.cancelDisposable)
}
override fun block(id: String) {
@ -126,7 +126,6 @@ class TimelineCasesImpl(
override fun onFailure(call: Call<Relationship>, t: Throwable) {}
})
eventHub.dispatch(BlockEvent(id))
}
override fun delete(id: String): Single<DeletedStatus> {

@ -66,8 +66,7 @@ class TimelineRepositoryImpl(
sinceIdMinusOne: String?, limit: Int,
accountId: Long, requestMode: TimelineRequestMode
): Single<out List<TimelineStatus>> {
val withMuted = true // TODO: configurable
return mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1, withMuted)
return mastodonApi.homeTimelineSingle(maxId, sinceIdMinusOne, limit + 1)
.map { statuses ->
this.saveStatusesToDb(accountId, statuses, maxId, sinceId)
}

@ -34,6 +34,7 @@ object PrefKeys {
const val BIG_EMOJIS = "bigEmojis"
const val STICKERS = "stickers"
const val ANONYMIZE_FILENAMES = "anonymizeFilenames"
const val HIDE_MUTED_USERS = "hideMutedUsers"
const val CUSTOM_TABS = "customTabs"

@ -247,7 +247,8 @@ class AccountViewModel @Inject constructor(
when (relationshipAction) {
RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(accountId))
RelationShipAction.BLOCK -> eventHub.dispatch(BlockEvent(accountId))
RelationShipAction.MUTE -> eventHub.dispatch(MuteEvent(accountId))
RelationShipAction.MUTE -> eventHub.dispatch(MuteEvent(accountId, true))
RelationShipAction.UNMUTE -> eventHub.dispatch(MuteEvent(accountId, false))
else -> {
}
}
@ -276,7 +277,6 @@ class AccountViewModel @Inject constructor(
call.enqueue(callback)
callList.add(call)
}
override fun onCleared() {
@ -299,7 +299,6 @@ class AccountViewModel @Inject constructor(
if (!isSelf)
obtainRelationship(isReload)
}
}
fun setAccountInfo(accountId: String) {

Loading…
Cancel
Save