From 909131960a8940b9bbf9154e4a8d49defb5f0f31 Mon Sep 17 00:00:00 2001 From: sk Date: Mon, 26 Jun 2023 22:53:33 +0200 Subject: [PATCH] what the fuck --- .../utils/StatusFilterPredicateTest.java | 8 +- .../android/api/CacheController.java | 61 ++++-- .../api/requests/accounts/GetWordFilters.java | 4 +- .../UpdateAccountCredentialsPreferences.java | 34 +++ .../api/requests/markers/GetMarkers.java | 17 +- .../api/session/AccountLocalPreferences.java | 40 ++++ .../android/api/session/AccountSession.java | 197 +++++++++++++++++- .../api/session/AccountSessionManager.java | 24 +-- .../NotificationsMarkerUpdatedEvent.java | 13 ++ .../StatusDisplaySettingsChangedEvent.java | 9 + .../fragments/AccountTimelineFragment.java | 1 - .../BookmarkedStatusListFragment.java | 1 - .../FavoritedStatusListFragment.java | 1 - .../fragments/HashtagTimelineFragment.java | 1 - .../android/fragments/HomeFragment.java | 183 +++++++++------- .../fragments/HomeTimelineFragment.java | 5 - .../fragments/ListTimelineFragment.java | 1 - .../fragments/NotificationsFragment.java | 31 ++- .../fragments/NotificationsListFragment.java | 131 ++++++++---- .../fragments/PinnedPostsListFragment.java | 1 - .../ScheduledStatusListFragment.java | 2 +- .../fragments/StatusEditHistoryFragment.java | 3 +- .../android/fragments/StatusListFragment.java | 4 +- .../discover/BubbleTimelineFragment.java | 2 - .../discover/DiscoverPostsFragment.java | 1 - .../discover/FederatedTimelineFragment.java | 2 - .../discover/LocalTimelineFragment.java | 2 - .../fragments/discover/SearchFragment.java | 3 +- .../report/ReportAddPostsChoiceFragment.java | 1 - .../android/model/FilterResult.java | 2 +- .../model/{Filter.java => LegacyFilter.java} | 2 +- .../joinmastodon/android/model/Markers.java | 14 -- .../android/model/TimelineMarkers.java | 13 ++ .../displayitems/HeaderStatusDisplayItem.java | 8 +- .../MediaGridStatusDisplayItem.java | 4 +- .../NotificationHeaderStatusDisplayItem.java | 175 ++++++++++++++++ .../PollOptionStatusDisplayItem.java | 5 +- .../SpoilerStatusDisplayItem.java | 1 + .../ui/displayitems/StatusDisplayItem.java | 45 +++- .../WarningFilteredStatusDisplayItem.java | 6 +- .../ui/views/CheckIconSelectableTextView.java | 39 ++++ .../ui/views/NestedRecyclerScrollView.java | 88 ++++++-- .../views/TopBarsScrollAwayLinearLayout.java | 29 +++ .../android/utils/ObjectIdComparator.java | 18 ++ .../android/utils/StatusFilterPredicate.java | 16 +- .../main/res/color/text_segmented_button.xml | 5 + .../main/res/drawable/bg_segmented_button.xml | 17 ++ .../src/main/res/drawable/bg_tabbar_badge.xml | 6 + .../src/main/res/drawable/bg_tabbar_tab.xml | 21 ++ .../res/drawable/divider_vertical_outline.xml | 5 + .../fg_segmented_button_container.xml | 5 + .../main/res/drawable/ic_done_all_24px.xml | 9 + .../res/drawable/ic_fluent_add_24_filled.xml | 3 + .../res/drawable/ic_fluent_edit_24_filled.xml | 3 + .../main/res/drawable/image_placeholder.xml | 4 + .../display_item_notification_header.xml | 35 ++++ mastodon/src/main/res/layout/tab_bar.xml | 135 +++++++----- mastodon/src/main/res/menu/notifications.xml | 5 + mastodon/src/main/res/values/attrs.xml | 4 + mastodon/src/main/res/values/ids.xml | 1 + mastodon/src/main/res/values/strings.xml | 11 +- mastodon/src/main/res/values/strings_sk.xml | 4 +- mastodon/src/main/res/values/styles.xml | 43 ++++ 63 files changed, 1222 insertions(+), 342 deletions(-) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/UpdateAccountCredentialsPreferences.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/api/session/AccountLocalPreferences.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/events/NotificationsMarkerUpdatedEvent.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/events/StatusDisplaySettingsChangedEvent.java rename mastodon/src/main/java/org/joinmastodon/android/model/{Filter.java => LegacyFilter.java} (97%) delete mode 100644 mastodon/src/main/java/org/joinmastodon/android/model/Markers.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/model/TimelineMarkers.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/NotificationHeaderStatusDisplayItem.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/views/CheckIconSelectableTextView.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/ui/views/TopBarsScrollAwayLinearLayout.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/utils/ObjectIdComparator.java create mode 100644 mastodon/src/main/res/color/text_segmented_button.xml create mode 100644 mastodon/src/main/res/drawable/bg_segmented_button.xml create mode 100644 mastodon/src/main/res/drawable/bg_tabbar_badge.xml create mode 100644 mastodon/src/main/res/drawable/bg_tabbar_tab.xml create mode 100644 mastodon/src/main/res/drawable/divider_vertical_outline.xml create mode 100644 mastodon/src/main/res/drawable/fg_segmented_button_container.xml create mode 100644 mastodon/src/main/res/drawable/ic_done_all_24px.xml create mode 100644 mastodon/src/main/res/drawable/ic_fluent_add_24_filled.xml create mode 100644 mastodon/src/main/res/drawable/ic_fluent_edit_24_filled.xml create mode 100644 mastodon/src/main/res/drawable/image_placeholder.xml create mode 100644 mastodon/src/main/res/layout/display_item_notification_header.xml diff --git a/mastodon/src/androidTest/java/org/joinmastodon/android/utils/StatusFilterPredicateTest.java b/mastodon/src/androidTest/java/org/joinmastodon/android/utils/StatusFilterPredicateTest.java index 252317a4dc..84f156706e 100644 --- a/mastodon/src/androidTest/java/org/joinmastodon/android/utils/StatusFilterPredicateTest.java +++ b/mastodon/src/androidTest/java/org/joinmastodon/android/utils/StatusFilterPredicateTest.java @@ -1,10 +1,10 @@ package org.joinmastodon.android.utils; -import static org.joinmastodon.android.model.Filter.FilterAction.*; +import static org.joinmastodon.android.model.FilterAction.*; import static org.joinmastodon.android.model.FilterContext.*; import static org.junit.Assert.*; -import org.joinmastodon.android.model.Filter; +import org.joinmastodon.android.model.LegacyFilter; import org.joinmastodon.android.model.Status; import org.junit.Test; @@ -14,8 +14,8 @@ public class StatusFilterPredicateTest { - private static final Filter hideMeFilter = new Filter(), warnMeFilter = new Filter(); - private static final List allFilters = List.of(hideMeFilter, warnMeFilter); + private static final LegacyFilter hideMeFilter = new LegacyFilter(), warnMeFilter = new LegacyFilter(); + private static final List allFilters = List.of(hideMeFilter, warnMeFilter); private static final Status hideInHomePublic = Status.ofFake(null, "hide me, please", Instant.now()), diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java b/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java index d45e36a9f0..545fae4997 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java @@ -16,10 +16,11 @@ import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.model.CacheablePaginatedResponse; -import org.joinmastodon.android.model.Filter; +import org.joinmastodon.android.model.LegacyFilter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Notification; +import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.model.SearchResult; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.utils.StatusFilterPredicate; @@ -44,6 +45,8 @@ public class CacheController{ private final String accountID; private DatabaseHelper db; private final Runnable databaseCloseRunnable=this::closeDatabase; + private boolean loadingNotifications; + private final ArrayList>>> pendingNotificationsCallbacks=new ArrayList<>(); private static final int POST_FLAG_GAP_AFTER=1; @@ -59,7 +62,7 @@ public void getHomeTimeline(String maxID, int count, boolean forceReload, Callba cancelDelayedClose(); databaseThread.postRunnable(()->{ try{ - List filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.HOME)).collect(Collectors.toList()); + List filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(FilterContext.HOME)).collect(Collectors.toList()); if(!forceReload){ SQLiteDatabase db=getOrOpenDatabase(); try(Cursor cursor=db.query("home_timeline", new String[]{"json", "flags"}, maxID==null ? null : "`id` posts, boolean clear){ }); } - public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback>> callback){ + public void getNotifications(String maxID, int count, boolean onlyMentions, boolean onlyPosts, boolean forceReload, Callback>> callback){ cancelDelayedClose(); databaseThread.postRunnable(()->{ try{ - AccountSession accountSession=AccountSessionManager.getInstance().getAccount(accountID); - List filters=accountSession.wordFilters.stream().filter(f->f.context.contains(FilterContext.NOTIFICATIONS)).collect(Collectors.toList()); + if(!onlyMentions && loadingNotifications){ + synchronized(pendingNotificationsCallbacks){ + pendingNotificationsCallbacks.add(callback); + } + return; + } if(!forceReload){ SQLiteDatabase db=getOrOpenDatabase(); - String table=onlyPosts ? "notifications_posts" : onlyMentions ? "notifications_mentions" : "notifications_all"; - try(Cursor cursor=db.query(table, new String[]{"json"}, maxID==null ? null : "`id` result=new ArrayList<>(); cursor.moveToFirst(); String newMaxID; - outer: do{ Notification ntf=MastodonAPIController.gson.fromJson(cursor.getString(0), Notification.class); ntf.postprocess(); newMaxID=ntf.id; - if(ntf.status!=null){ - if (!new StatusFilterPredicate(filters, FilterContext.NOTIFICATIONS).test(ntf.status)) - continue outer; - } result.add(ntf); }while(cursor.moveToNext()); String _newMaxID=newMaxID; - uiHandler.post(()->callback.onSuccess(new CacheablePaginatedResponse<>(result, _newMaxID, true))); + AccountSessionManager.get(accountID).filterStatusContainingObjects(result, n->n.status, FilterContext.NOTIFICATIONS); + uiHandler.post(()->callback.onSuccess(new PaginatedResponse<>(result, _newMaxID))); return; } }catch(IOException x){ Log.w(TAG, "getNotifications: corrupted notification object in database", x); } } - Instance instance=AccountSessionManager.getInstance().getInstanceInfo(accountSession.domain); - new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), instance.isAkkoma()) + if(!onlyMentions) + loadingNotifications=true; + new GetNotifications(maxID, count, onlyPosts ? EnumSet.of(Notification.Type.STATUS) : onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class), AccountSessionManager.get(accountID).getInstance().map(Instance::isAkkoma).orElse(false)) .setCallback(new Callback<>(){ @Override public void onSuccess(List result){ - callback.onSuccess(new CacheablePaginatedResponse<>(result.stream().filter(ntf->{ - if(ntf.status!=null){ - return new StatusFilterPredicate(filters, FilterContext.NOTIFICATIONS).test(ntf.status); - } - return true; - }).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id, false)); + ArrayList filtered=new ArrayList<>(result); + AccountSessionManager.get(accountID).filterStatusContainingObjects(filtered, n->n.status, FilterContext.NOTIFICATIONS); + PaginatedResponse> res=new PaginatedResponse<>(filtered, result.isEmpty() ? null : result.get(result.size()-1).id); + callback.onSuccess(res); putNotifications(result, onlyMentions, onlyPosts, maxID==null); + if(!onlyMentions){ + loadingNotifications=false; + synchronized(pendingNotificationsCallbacks){ + for(Callback>> cb:pendingNotificationsCallbacks){ + cb.onSuccess(res); + } + pendingNotificationsCallbacks.clear(); + } + } } @Override public void onError(ErrorResponse error){ callback.onError(error); + if(!onlyMentions){ + loadingNotifications=false; + synchronized(pendingNotificationsCallbacks){ + for(Callback>> cb:pendingNotificationsCallbacks){ + cb.onError(error); + } + pendingNotificationsCallbacks.clear(); + } + } } }) .exec(accountID); diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetWordFilters.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetWordFilters.java index 781035959b..639b51ef54 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetWordFilters.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetWordFilters.java @@ -3,11 +3,11 @@ import com.google.gson.reflect.TypeToken; import org.joinmastodon.android.api.MastodonAPIRequest; -import org.joinmastodon.android.model.Filter; +import org.joinmastodon.android.model.LegacyFilter; import java.util.List; -public class GetWordFilters extends MastodonAPIRequest>{ +public class GetWordFilters extends MastodonAPIRequest>{ public GetWordFilters(){ super(HttpMethod.GET, "/filters", new TypeToken<>(){}); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/UpdateAccountCredentialsPreferences.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/UpdateAccountCredentialsPreferences.java new file mode 100644 index 0000000000..686b64e3f9 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/UpdateAccountCredentialsPreferences.java @@ -0,0 +1,34 @@ +package org.joinmastodon.android.api.requests.accounts; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.Preferences; +import org.joinmastodon.android.model.StatusPrivacy; + +public class UpdateAccountCredentialsPreferences extends MastodonAPIRequest{ + public UpdateAccountCredentialsPreferences(Preferences preferences, Boolean locked, Boolean discoverable){ + super(HttpMethod.PATCH, "/accounts/update_credentials", Account.class); + setRequestBody(new Request(locked, discoverable, new RequestSource(preferences.postingDefaultVisibility, preferences.postingDefaultLanguage))); + } + + private static class Request{ + public Boolean locked, discoverable; + public RequestSource source; + + public Request(Boolean locked, Boolean discoverable, RequestSource source){ + this.locked=locked; + this.discoverable=discoverable; + this.source=source; + } + } + + private static class RequestSource{ + public StatusPrivacy privacy; + public String language; + + public RequestSource(StatusPrivacy privacy, String language){ + this.privacy=privacy; + this.language=language; + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/markers/GetMarkers.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/markers/GetMarkers.java index b7dd6536b1..369de0b4d6 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/markers/GetMarkers.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/markers/GetMarkers.java @@ -1,17 +1,12 @@ package org.joinmastodon.android.api.requests.markers; -import org.joinmastodon.android.api.ApiUtils; import org.joinmastodon.android.api.MastodonAPIRequest; -import org.joinmastodon.android.model.Marker; -import org.joinmastodon.android.model.Markers; +import org.joinmastodon.android.model.TimelineMarkers; -import java.util.EnumSet; - -public class GetMarkers extends MastodonAPIRequest { - public GetMarkers(EnumSet timelines) { - super(HttpMethod.GET, "/markers", Markers.class); - for (String type : ApiUtils.enumSetToStrings(timelines, Marker.Type.class)){ - addQueryParameter("timeline[]", type); - } +public class GetMarkers extends MastodonAPIRequest { + public GetMarkers(){ + super(HttpMethod.GET, "/markers", TimelineMarkers.class); + addQueryParameter("timeline[]", "home"); + addQueryParameter("timeline[]", "notifications"); } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountLocalPreferences.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountLocalPreferences.java new file mode 100644 index 0000000000..a1eb474f18 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountLocalPreferences.java @@ -0,0 +1,40 @@ +package org.joinmastodon.android.api.session; + +import android.content.SharedPreferences; + +public class AccountLocalPreferences{ + private final SharedPreferences prefs; + + public boolean showInteractionCounts; + public boolean customEmojiInNames; + public boolean showCWs; + public boolean hideSensitiveMedia; + public boolean serverSideFiltersSupported; + + public AccountLocalPreferences(SharedPreferences prefs){ + this.prefs=prefs; + showInteractionCounts=prefs.getBoolean("interactionCounts", true); + customEmojiInNames=prefs.getBoolean("emojiInNames", true); + showCWs=prefs.getBoolean("showCWs", true); + hideSensitiveMedia=prefs.getBoolean("hideSensitive", true); + serverSideFiltersSupported=prefs.getBoolean("serverSideFilters", false); + } + + public long getNotificationsPauseEndTime(){ + return prefs.getLong("notificationsPauseTime", 0L); + } + + public void setNotificationsPauseEndTime(long time){ + prefs.edit().putLong("notificationsPauseTime", time).apply(); + } + + public void save(){ + prefs.edit() + .putBoolean("interactionCounts", showInteractionCounts) + .putBoolean("emojiInNames", customEmojiInNames) + .putBoolean("showCWs", showCWs) + .putBoolean("hideSensitive", hideSensitiveMedia) + .putBoolean("serverSideFilters", serverSideFiltersSupported) + .apply(); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java index 30acb30d6b..40754ccdd5 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSession.java @@ -1,25 +1,51 @@ package org.joinmastodon.android.api.session; +import android.app.Activity; +import android.content.Context; +import android.content.SharedPreferences; import android.net.Uri; +import android.text.TextUtils; +import android.util.Log; +import org.joinmastodon.android.E; +import org.joinmastodon.android.MastodonApp; +import org.joinmastodon.android.R; import org.joinmastodon.android.api.CacheController; import org.joinmastodon.android.api.MastodonAPIController; import org.joinmastodon.android.api.PushSubscriptionManager; import org.joinmastodon.android.api.StatusInteractionController; +import org.joinmastodon.android.api.requests.accounts.GetPreferences; +import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentialsPreferences; +import org.joinmastodon.android.api.requests.markers.GetMarkers; +import org.joinmastodon.android.api.requests.markers.SaveMarkers; +import org.joinmastodon.android.api.requests.oauth.RevokeOauthToken; +import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Application; -import org.joinmastodon.android.model.Filter; +import org.joinmastodon.android.model.FilterAction; +import org.joinmastodon.android.model.FilterContext; +import org.joinmastodon.android.model.FilterResult; import org.joinmastodon.android.model.Instance; -import org.joinmastodon.android.model.Markers; +import org.joinmastodon.android.model.LegacyFilter; import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.PushSubscription; +import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.model.TimelineMarkers; import org.joinmastodon.android.model.Token; +import org.joinmastodon.android.utils.ObjectIdComparator; import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; + +import me.grishka.appkit.api.Callback; +import me.grishka.appkit.api.ErrorResponse; public class AccountSession{ + private static final String TAG="AccountSession"; + public Token token; public Account self; public String domain; @@ -32,15 +58,17 @@ public class AccountSession{ public PushSubscription pushSubscription; public boolean needUpdatePushSettings; public long filtersLastUpdated; - public List wordFilters=new ArrayList<>(); + public List wordFilters=new ArrayList<>(); public String pushAccountID; - public Preferences preferences; public AccountActivationInfo activationInfo; - public Markers markers; + public Preferences preferences; private transient MastodonAPIController apiController; private transient StatusInteractionController statusInteractionController, remoteStatusInteractionController; private transient CacheController cacheController; private transient PushSubscriptionManager pushSubscriptionManager; + private transient SharedPreferences prefs; + private transient boolean preferencesNeedSaving; + private transient AccountLocalPreferences localPreferences; AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){ this.token=token; @@ -58,10 +86,6 @@ public String getID(){ return domain+"_"+self.id; } - public String getFullUsername() { - return "@"+self.username+"@"+domain; - } - public MastodonAPIController getApiController(){ if(apiController==null) apiController=new MastodonAPIController(this); @@ -92,6 +116,161 @@ public PushSubscriptionManager getPushSubscriptionManager(){ return pushSubscriptionManager; } + public String getFullUsername(){ + return '@'+self.username+'@'+domain; + } + + public void reloadPreferences(Consumer callback){ + new GetPreferences() + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Preferences result){ + preferences=result; + if(callback!=null) + callback.accept(result); + AccountSessionManager.getInstance().writeAccountsFile(); + } + + @Override + public void onError(ErrorResponse error){ + Log.w(TAG, "Failed to load preferences for account "+getID()+": "+error); + } + }) + .exec(getID()); + } + + public SharedPreferences getRawLocalPreferences(){ + if(prefs==null) + prefs=MastodonApp.context.getSharedPreferences(getID(), Context.MODE_PRIVATE); + return prefs; + } + + public void reloadNotificationsMarker(Consumer callback){ + new GetMarkers() + .setCallback(new Callback<>(){ + @Override + public void onSuccess(TimelineMarkers result){ + if(result.notifications!=null && !TextUtils.isEmpty(result.notifications.lastReadId)){ + String id=result.notifications.lastReadId; + String lastKnown=getLastKnownNotificationsMarker(); + if(ObjectIdComparator.INSTANCE.compare(id, lastKnown)<0){ + // Marker moved back -- previous marker update must have failed. + // Pretend it didn't happen and repeat the request. + id=lastKnown; + new SaveMarkers(null, id).exec(getID()); + } + callback.accept(id); + setNotificationsMarker(id, false); + } + } + + @Override + public void onError(ErrorResponse error){} + }) + .exec(getID()); + } + + public String getLastKnownNotificationsMarker(){ + return getRawLocalPreferences().getString("notificationsMarker", null); + } + + public void setNotificationsMarker(String id, boolean clearUnread){ + getRawLocalPreferences().edit().putString("notificationsMarker", id).apply(); + E.post(new NotificationsMarkerUpdatedEvent(getID(), id, clearUnread)); + } + + public void logOut(Activity activity, Runnable onDone){ + new RevokeOauthToken(app.clientId, app.clientSecret, token.accessToken) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Object result){ + AccountSessionManager.getInstance().removeAccount(getID()); + onDone.run(); + } + + @Override + public void onError(ErrorResponse error){ + AccountSessionManager.getInstance().removeAccount(getID()); + onDone.run(); + } + }) + .wrapProgress(activity, R.string.loading, false) + .exec(getID()); + } + + public void savePreferencesLater(){ + preferencesNeedSaving=true; + } + + public void savePreferencesIfPending(){ + if(preferencesNeedSaving){ + new UpdateAccountCredentialsPreferences(preferences, null, null) + .setCallback(new Callback<>(){ + @Override + public void onSuccess(Account result){ + preferencesNeedSaving=false; + self=result; + AccountSessionManager.getInstance().writeAccountsFile(); + } + + @Override + public void onError(ErrorResponse error){ + Log.e(TAG, "failed to save preferences: "+error); + } + }) + .exec(getID()); + } + } + + public AccountLocalPreferences getLocalPreferences(){ + if(localPreferences==null) + localPreferences=new AccountLocalPreferences(getRawLocalPreferences()); + return localPreferences; + } + + public void filterStatuses(List statuses, FilterContext context){ + filterStatusContainingObjects(statuses, Function.identity(), context); + } + + public void filterStatusContainingObjects(List objects, Function extractor, FilterContext context){ + if(getLocalPreferences().serverSideFiltersSupported){ + // Even with server-side filters, clients are expected to remove statuses that match a filter that hides them + objects.removeIf(o->{ + Status s=extractor.apply(o); + if(s==null) + return false; + if(s.filtered==null) + return false; + for(FilterResult filter:s.filtered){ + if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE) + return true; + } + return false; + }); + return; + } + if(wordFilters==null) + return; + for(T obj:objects){ + Status s=extractor.apply(obj); + if(s!=null && s.filtered!=null){ + getLocalPreferences().serverSideFiltersSupported=true; + getLocalPreferences().save(); + return; + } + } + objects.removeIf(o->{ + Status s=extractor.apply(o); + if(s==null) + return false; + for(LegacyFilter filter:wordFilters){ + if(filter.context.contains(context) && filter.matches(s) && filter.isActive()) + return true; + } + return false; + }); + } + public Optional getInstance() { return Optional.ofNullable(AccountSessionManager.getInstance().getInstanceInfo(domain)); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java index 9e09f7630d..f1658e7c62 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/session/AccountSessionManager.java @@ -26,17 +26,14 @@ import org.joinmastodon.android.api.requests.instance.GetCustomEmojis; import org.joinmastodon.android.api.requests.accounts.GetOwnAccount; import org.joinmastodon.android.api.requests.instance.GetInstance; -import org.joinmastodon.android.api.requests.markers.GetMarkers; import org.joinmastodon.android.api.requests.oauth.CreateOAuthApp; import org.joinmastodon.android.events.EmojiUpdatedEvent; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Application; import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.EmojiCategory; -import org.joinmastodon.android.model.Filter; +import org.joinmastodon.android.model.LegacyFilter; import org.joinmastodon.android.model.Instance; -import org.joinmastodon.android.model.Marker; -import org.joinmastodon.android.model.Markers; import org.joinmastodon.android.model.Preferences; import org.joinmastodon.android.model.Token; @@ -50,7 +47,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -282,7 +278,6 @@ public void maybeUpdateLocalInfo(AccountSession activeSession){ if(now-session.filtersLastUpdated>3600_000L || session == activeSession){ updateSessionWordFilters(session); } - updateSessionMarkers(session); } if(loadedInstances){ maybeUpdateCustomEmojis(domains, activeSession != null ? activeSession.domain : null); @@ -345,7 +340,7 @@ private void updateSessionWordFilters(AccountSession session){ new GetWordFilters() .setCallback(new Callback<>(){ @Override - public void onSuccess(List result){ + public void onSuccess(List result){ session.wordFilters=result; session.filtersLastUpdated=System.currentTimeMillis(); writeAccountsFile(); @@ -359,21 +354,6 @@ public void onError(ErrorResponse error){ .exec(session.getID()); } - private void updateSessionMarkers(AccountSession session) { - new GetMarkers(EnumSet.allOf(Marker.Type.class)).setCallback(new Callback<>() { - @Override - public void onSuccess(Markers markers) { - session.markers = markers; - writeAccountsFile(); - } - - @Override - public void onError(ErrorResponse error) { - - } - }).exec(session.getID()); - } - public void updateInstanceInfo(String domain){ new GetInstance() .setCallback(new Callback<>(){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/events/NotificationsMarkerUpdatedEvent.java b/mastodon/src/main/java/org/joinmastodon/android/events/NotificationsMarkerUpdatedEvent.java new file mode 100644 index 0000000000..f68a5b99d6 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/events/NotificationsMarkerUpdatedEvent.java @@ -0,0 +1,13 @@ +package org.joinmastodon.android.events; + +public class NotificationsMarkerUpdatedEvent{ + public final String accountID; + public final String marker; + public final boolean clearUnread; + + public NotificationsMarkerUpdatedEvent(String accountID, String marker, boolean clearUnread){ + this.accountID=accountID; + this.marker=marker; + this.clearUnread=clearUnread; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/events/StatusDisplaySettingsChangedEvent.java b/mastodon/src/main/java/org/joinmastodon/android/events/StatusDisplaySettingsChangedEvent.java new file mode 100644 index 0000000000..b4f63a0987 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/events/StatusDisplaySettingsChangedEvent.java @@ -0,0 +1,9 @@ +package org.joinmastodon.android.events; + +public class StatusDisplaySettingsChangedEvent{ + public final String accountID; + + public StatusDisplaySettingsChangedEvent(String accountID){ + this.accountID=accountID; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java index c2feaf8883..53f594f325 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/AccountTimelineFragment.java @@ -12,7 +12,6 @@ import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.events.StatusUnpinnedEvent; import org.joinmastodon.android.model.Account; -import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/BookmarkedStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/BookmarkedStatusListFragment.java index eba8da713a..e745aed2a4 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/BookmarkedStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/BookmarkedStatusListFragment.java @@ -5,7 +5,6 @@ import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.statuses.GetBookmarkedStatuses; -import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.HeaderPaginationList; import org.joinmastodon.android.model.Status; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/FavoritedStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/FavoritedStatusListFragment.java index 258cc53175..d5c2577af5 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/FavoritedStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/FavoritedStatusListFragment.java @@ -5,7 +5,6 @@ import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.statuses.GetFavoritedStatuses; -import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.HeaderPaginationList; import org.joinmastodon.android.model.Status; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java index 998bbe33c9..6d335c4a37 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HashtagTimelineFragment.java @@ -17,7 +17,6 @@ import org.joinmastodon.android.api.requests.tags.SetHashtagFollowed; import org.joinmastodon.android.api.requests.timelines.GetHashtagTimeline; import org.joinmastodon.android.events.HashtagUpdatedEvent; -import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.Status; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java index fbbfb26f39..28db37316c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java @@ -1,50 +1,52 @@ package org.joinmastodon.android.fragments; +import android.annotation.SuppressLint; import android.app.Fragment; import android.app.NotificationManager; import android.app.assist.AssistContent; -import android.graphics.Outline; import android.os.Build; import android.os.Bundle; -import android.service.notification.StatusBarNotification; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.view.ViewOutlineProvider; import android.view.ViewTreeObserver; import android.view.WindowInsets; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; - -import androidx.annotation.IdRes; -import androidx.annotation.Nullable; +import android.widget.TextView; import com.squareup.otto.Subscribe; +import org.joinmastodon.android.BuildConfig; import org.joinmastodon.android.E; +import org.joinmastodon.android.PushNotificationReceiver; import org.joinmastodon.android.R; -import org.joinmastodon.android.api.requests.notifications.GetNotifications; import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; -import org.joinmastodon.android.events.AllNotificationsSeenEvent; -import org.joinmastodon.android.events.NotificationReceivedEvent; +import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent; +import org.joinmastodon.android.events.StatusDisplaySettingsChangedEvent; import org.joinmastodon.android.fragments.discover.DiscoverFragment; +import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Instance; import org.joinmastodon.android.model.Notification; +import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.ui.AccountSwitcherSheet; +import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.TabBar; +import org.joinmastodon.android.utils.ObjectIdComparator; import org.joinmastodon.android.utils.ProvidesAssistContent; import org.parceler.Parcels; import java.util.ArrayList; -import java.util.EnumSet; import java.util.List; -import java.util.Optional; +import androidx.annotation.IdRes; +import androidx.annotation.Nullable; import me.grishka.appkit.FragmentStackActivity; +import me.grishka.appkit.Nav; import me.grishka.appkit.api.Callback; import me.grishka.appkit.api.ErrorResponse; import me.grishka.appkit.fragments.AppKitFragment; @@ -64,34 +66,30 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene private TabBar tabBar; private View tabBarWrap; private ImageView tabBarAvatar; - private ImageView notificationTabIcon; @IdRes private int currentTab=R.id.tab_home; + private TextView notificationsBadge; private String accountID; - private boolean isPleroma; + private boolean isAkkoma; @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); - E.register(this); accountID=getArguments().getString("account"); - setTitle(R.string.sk_app_name); - isPleroma = AccountSessionManager.getInstance().getAccount(accountID).getInstance() - .map(Instance::isAkkoma) - .orElse(false); + setTitle(R.string.app_name); + isAkkoma = AccountSessionManager.get(accountID).getInstance().map(Instance::isAkkoma).orElse(false); if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) setRetainInstance(true); - // TODO: clean up if(savedInstanceState==null){ Bundle args=new Bundle(); args.putString("account", accountID); homeTabFragment=new HomeTabFragment(); homeTabFragment.setArguments(args); args=new Bundle(args); - args.putBoolean("disableDiscover", isPleroma); + args.putBoolean("disableDiscover", isAkkoma); args.putBoolean("noAutoLoad", true); searchFragment=new DiscoverFragment(); searchFragment.setArguments(args); @@ -104,6 +102,13 @@ public void onCreate(Bundle savedInstanceState){ profileFragment.setArguments(args); } + E.register(this); + } + + @Override + public void onDestroy(){ + super.onDestroy(); + E.unregister(this); } @Nullable @@ -113,7 +118,7 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, content.setOrientation(LinearLayout.VERTICAL); FrameLayout fragmentContainer=new FrameLayout(getActivity()); - fragmentContainer.setId(me.grishka.appkit.R.id.fragment_wrap); + fragmentContainer.setId(R.id.fragment_wrap); content.addView(fragmentContainer, new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f)); inflater.inflate(R.layout.tab_bar, content); @@ -122,25 +127,20 @@ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, tabBarWrap=content.findViewById(R.id.tabbar_wrap); tabBarAvatar=tabBar.findViewById(R.id.tab_profile_ava); - tabBarAvatar.setOutlineProvider(new ViewOutlineProvider(){ - @Override - public void getOutline(View view, Outline outline){ - outline.setOval(0, 0, view.getWidth(), view.getHeight()); - } - }); + tabBarAvatar.setOutlineProvider(OutlineProviders.OVAL); tabBarAvatar.setClipToOutline(true); Account self=AccountSessionManager.getInstance().getAccount(accountID).self; - ViewImageLoader.load(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(28), V.dp(28))); + ViewImageLoader.loadWithoutAnimation(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24))); - notificationTabIcon=content.findViewById(R.id.tab_notifications); - updateNotificationBadge(); + notificationsBadge=tabBar.findViewById(R.id.notifications_badge); + notificationsBadge.setVisibility(View.GONE); if(savedInstanceState==null){ getChildFragmentManager().beginTransaction() - .add(me.grishka.appkit.R.id.fragment_wrap, homeTabFragment) - .add(me.grishka.appkit.R.id.fragment_wrap, searchFragment).hide(searchFragment) - .add(me.grishka.appkit.R.id.fragment_wrap, notificationsFragment).hide(notificationsFragment) - .add(me.grishka.appkit.R.id.fragment_wrap, profileFragment).hide(profileFragment) + .add(R.id.fragment_wrap, homeTabFragment) + .add(R.id.fragment_wrap, searchFragment).hide(searchFragment) + .add(R.id.fragment_wrap, notificationsFragment).hide(notificationsFragment) + .add(R.id.fragment_wrap, profileFragment).hide(profileFragment) .commit(); String defaultTab=getArguments().getString("tab"); @@ -163,7 +163,8 @@ public boolean onPreDraw(){ @Override public void onViewStateRestored(Bundle savedInstanceState){ super.onViewStateRestored(savedInstanceState); - if(savedInstanceState==null) return; + if(savedInstanceState==null || homeTabFragment!=null) + return; homeTabFragment=(HomeTabFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTabFragment"); searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment"); notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment"); @@ -201,7 +202,7 @@ public boolean wantsLightNavigationBar(){ public void onApplyWindowInsets(WindowInsets insets){ if(Build.VERSION.SDK_INT>=27){ int inset=insets.getSystemWindowInsetBottom(); - tabBarWrap.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(36)) : 0); + tabBarWrap.setPadding(0, 0, 0, inset>0 ? Math.max(inset, V.dp(24)) : 0); super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), 0)); }else{ super.onApplyWindowInsets(insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), 0, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom())); @@ -247,7 +248,7 @@ else if(newFragment instanceof ScrollableToTop scrollable) if (newFragment instanceof HasFab fabulous && !fabulous.isScrolling()) fabulous.showFab(); currentTab=tab; ((FragmentStackActivity)getActivity()).invalidateSystemBarColors(this); - if (tab == R.id.tab_search && isPleroma) searchFragment.selectSearch(); + if (tab == R.id.tab_search && isAkkoma) searchFragment.selectSearch(); } private void maybeTriggerLoading(Fragment newFragment){ @@ -258,13 +259,8 @@ private void maybeTriggerLoading(Fragment newFragment){ ((DiscoverFragment) newFragment).loadData(); }else if(newFragment instanceof NotificationsFragment){ ((NotificationsFragment) newFragment).loadData(); - // TODO make an interface? NotificationManager nm=getActivity().getSystemService(NotificationManager.class); - for (StatusBarNotification notification : nm.getActiveNotifications()) { - if (accountID.equals(notification.getTag())) { - nm.cancel(accountID, notification.getId()); - } - } + nm.cancel(accountID, PushNotificationReceiver.NOTIFICATION_ID); } } @@ -277,6 +273,11 @@ private boolean onTabLongClick(@IdRes int tab){ new AccountSwitcherSheet(getActivity(), this).show(); return true; } + if(tab==R.id.tab_home && BuildConfig.DEBUG){ + Bundle args=new Bundle(); + args.putString("account", accountID); + Nav.go(getActivity(), OnboardingFollowSuggestionsFragment.class, args); + } return false; } @@ -299,53 +300,79 @@ public boolean onBackPressed(){ public void onSaveInstanceState(Bundle outState){ super.onSaveInstanceState(outState); outState.putInt("selectedTab", currentTab); - if (homeTabFragment.isAdded()) getChildFragmentManager().putFragment(outState, "homeTabFragment", homeTabFragment); - if (searchFragment.isAdded()) getChildFragmentManager().putFragment(outState, "searchFragment", searchFragment); - if (notificationsFragment.isAdded()) getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment); - if (profileFragment.isAdded()) getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment); + getChildFragmentManager().putFragment(outState, "homeTabFragment", homeTabFragment); + getChildFragmentManager().putFragment(outState, "searchFragment", searchFragment); + getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment); + getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment); } - public void updateNotificationBadge() { - AccountSession session = AccountSessionManager.getInstance().getAccount(accountID); - Optional instance = session.getInstance(); - if (instance.isEmpty()) return; // avoiding incompatibility with akkoma + @Override + protected void onShown(){ + super.onShown(); + reloadNotificationsForUnreadCount(); + } - new GetNotifications(null, 1, EnumSet.allOf(Notification.Type.class), instance.get().isAkkoma()) - .setCallback(new Callback<>() { - @Override - public void onSuccess(List notifications) { - if (notifications.size() > 0) { - try { - long newestId = Long.parseLong(notifications.get(0).id); - long lastSeenId = Long.parseLong(session.markers.notifications.lastReadId); - setNotificationBadge(newestId > lastSeenId); - } catch (Exception ignored) { - setNotificationBadge(false); - } - } - } + private void reloadNotificationsForUnreadCount(){ + List[] notifications=new List[]{null}; + String[] marker={null}; - @Override - public void onError(ErrorResponse error) { - setNotificationBadge(false); - } - }).exec(accountID); + AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{ + marker[0]=m; + if(notifications[0]!=null){ + updateUnreadCount(notifications[0], marker[0]); + } + }); + + AccountSessionManager.get(accountID).getCacheController().getNotifications(null, 40, false, false, true, new Callback<>(){ + @Override + public void onSuccess(PaginatedResponse> result){ + notifications[0]=result.items; + if(marker[0]!=null) + updateUnreadCount(notifications[0], marker[0]); + } + + @Override + public void onError(ErrorResponse error){} + }); } - public void setNotificationBadge(boolean badge) { - notificationTabIcon.setImageResource(badge - ? R.drawable.ic_fluent_alert_28_selector_badged - : R.drawable.ic_fluent_alert_28_selector); + @SuppressLint("DefaultLocale") + private void updateUnreadCount(List notifications, String marker){ + if(notifications.isEmpty() || ObjectIdComparator.INSTANCE.compare(notifications.get(0).id, marker)<=0){ + notificationsBadge.setVisibility(View.GONE); + }else{ + notificationsBadge.setVisibility(View.VISIBLE); + if(ObjectIdComparator.INSTANCE.compare(notifications.get(notifications.size()-1).id, marker)>0){ + notificationsBadge.setText(String.format("%d+", notifications.size())); + }else{ + int count=0; + for(Notification n:notifications){ + if(n.id.equals(marker)) + break; + count++; + } + notificationsBadge.setText(String.format("%d", count)); + } + } } @Subscribe - public void onNotificationReceived(NotificationReceivedEvent notificationReceivedEvent) { - if (notificationReceivedEvent.account.equals(accountID)) setNotificationBadge(true); + public void onNotificationsMarkerUpdated(NotificationsMarkerUpdatedEvent ev){ + if(!ev.accountID.equals(accountID)) + return; + if(ev.clearUnread) + notificationsBadge.setVisibility(View.GONE); } @Subscribe - public void onAllNotificationsSeen(AllNotificationsSeenEvent allNotificationsSeenEvent) { - setNotificationBadge(false); + public void onStatusDisplaySettingsChanged(StatusDisplaySettingsChangedEvent ev){ + if(!ev.accountID.equals(accountID)) + return; + // TODO +// if(homeTabFragment.loaded) +// homeTabFragment.rebuildAllDisplayItems(); +// if(notificationsFragment.loaded) +// notificationsFragment.rebuildAllDisplayItems(); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java index 7e9b83a5f9..b57ee46c5f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java @@ -14,7 +14,6 @@ import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.model.CacheablePaginatedResponse; -import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem; @@ -168,10 +167,6 @@ public void onError(ErrorResponse error){ } }) .exec(accountID); - - if (parent.getParentFragment() instanceof HomeFragment homeFragment) { - homeFragment.updateNotificationBadge(); - } } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelineFragment.java index 1acb2866af..13ecbc082b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ListTimelineFragment.java @@ -18,7 +18,6 @@ import org.joinmastodon.android.api.requests.timelines.GetListTimeline; import org.joinmastodon.android.events.ListDeletedEvent; import org.joinmastodon.android.events.ListUpdatedCreatedEvent; -import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.ListTimeline; import org.joinmastodon.android.model.Status; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java index fb589f1190..4d9caaf265 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java @@ -24,6 +24,8 @@ import org.joinmastodon.android.GlobalUserPreferences; import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.accounts.GetFollowRequests; +import org.joinmastodon.android.api.requests.markers.SaveMarkers; +import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.events.FollowRequestHandledEvent; import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.HeaderPaginationList; @@ -31,6 +33,7 @@ import org.joinmastodon.android.ui.tabs.TabLayout; import org.joinmastodon.android.ui.tabs.TabLayoutMediator; import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.utils.ObjectIdComparator; import org.joinmastodon.android.utils.ProvidesAssistContent; import me.grishka.appkit.Nav; @@ -49,6 +52,8 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc private NotificationsListFragment allNotificationsFragment, mentionsFragment; private String accountID; + private MenuItem markAllReadItem; + @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); @@ -76,7 +81,9 @@ public void onAttach(Activity activity){ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ inflater.inflate(R.menu.notifications, menu); menu.findItem(R.id.clear_notifications).setVisible(GlobalUserPreferences.enableDeleteNotifications); - UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.follow_requests); + markAllReadItem=menu.findItem(R.id.mark_all_read); + updateMarkAllReadButton(); + UiUtils.enableOptionsMenuIcons(getActivity(), menu, R.id.follow_requests, R.id.mark_all_read); } @Override @@ -93,10 +100,26 @@ public boolean onOptionsItemSelected(MenuItem item) { } }); return true; + } else if (item.getItemId()==R.id.mark_all_read){ + markAsRead(); } return false; } + void markAsRead(){ + Fragment f = getFragmentForPage(pager.getCurrentItem()); + if (f instanceof NotificationsListFragment n) { + n.resetUnreadBackground(); + String id = n.getData().get(0).id; + if(ObjectIdComparator.INSTANCE.compare(id, n.getRealUnreadMarker())>0){ + new SaveMarkers(null, id).exec(accountID); + AccountSessionManager.get(accountID).setNotificationsMarker(id, true); + n.setRealUnreadMarker(id); + if (getParentFragment() instanceof NotificationsFragment p) p.updateMarkAllReadButton(); + } + } + } + @Override public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ LinearLayout view=(LinearLayout) inflater.inflate(R.layout.fragment_notifications, container, false); @@ -184,6 +207,12 @@ public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){ return view; } + void updateMarkAllReadButton(){ + markAllReadItem.setVisible(false); // TODO: remove once it's working + markAllReadItem.setEnabled(!allNotificationsFragment.getData().isEmpty() && + !allNotificationsFragment.getRealUnreadMarker().equals(allNotificationsFragment.getData().get(0).id)); + } + public void refreshFollowRequestsBadge() { new GetFollowRequests(null, 1).setCallback(new Callback<>() { @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java index 33a8366a7b..c81a88b23c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java @@ -21,19 +21,22 @@ import org.joinmastodon.android.model.CacheablePaginatedResponse; import org.joinmastodon.android.model.Emoji; import org.joinmastodon.android.model.FilterContext; -import org.joinmastodon.android.model.Markers; +import org.joinmastodon.android.model.PaginatedResponse; +import org.joinmastodon.android.model.TimelineMarkers; import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; +import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem; import org.joinmastodon.android.ui.text.HtmlParser; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration; import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.utils.ObjectIdComparator; import org.parceler.Parcels; import java.util.ArrayList; @@ -52,6 +55,7 @@ public class NotificationsListFragment extends BaseStatusListFragment{ + unreadMarker=realUnreadMarker=m; + }); } @Override protected List buildDisplayItems(Notification n){ - Account reportTarget = n.report == null ? null : n.report.targetAccount == null ? null : - n.report.targetAccount; - Emoji emoji = new Emoji(); - if(n.emojiUrl!=null){ - emoji.shortcode=n.emoji.substring(1,n.emoji.length()-1); - emoji.url=n.emojiUrl; - emoji.staticUrl=n.emojiUrl; - emoji.visibleInPicker=false; + NotificationHeaderStatusDisplayItem titleItem; + if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){ + titleItem=null; + }else{ + titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID); + if(n.status!=null){ + n.status.card=null; + n.status.spoilerText=null; + } } - String extraText=switch(n.type){ - case FOLLOW -> getString(R.string.user_followed_you); - case FOLLOW_REQUEST -> getString(R.string.user_sent_follow_request); - case MENTION, STATUS -> null; - case REBLOG -> getString(R.string.notification_boosted); - case FAVORITE -> getString(R.string.user_favorited); - case POLL -> getString(R.string.poll_ended); - case UPDATE -> getString(R.string.sk_post_edited); - case SIGN_UP -> getString(R.string.sk_signed_up); - case REPORT -> getString(R.string.sk_reported); - case REACTION, PLEROMA_EMOJI_REACTION -> - n.emoji != null ? getString(R.string.sk_reacted_with, n.emoji) : getString(R.string.sk_reacted); - }; - HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, n.status, n.emojiUrl!=null ? HtmlParser.parseCustomEmoji(extraText, Collections.singletonList(emoji)) : extraText, n, null) : null; if(n.status!=null){ - ArrayList items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null, n, false, FilterContext.NOTIFICATIONS); + int flags=titleItem==null ? 0 : (StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET); // | StatusDisplayItem.FLAG_NO_HEADER); + ArrayList items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, null, flags); if(titleItem!=null) items.add(0, titleItem); return items; }else if(titleItem!=null){ - AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, - reportTarget != null ? reportTarget : n.account, n); - TextStatusDisplayItem text = n.report != null && !TextUtils.isEmpty(n.report.comment) ? - new TextStatusDisplayItem(n.id, n.report.comment, this, - Status.ofFake(n.id, n.report.comment, n.createdAt), true) : - null; - return text == null ? Arrays.asList(titleItem, card) : Arrays.asList(titleItem, text, card); + return Collections.singletonList(titleItem); }else{ return Collections.emptyList(); } @@ -140,11 +135,27 @@ protected void addAccountToKnown(Notification s){ @Override protected void doLoadData(int offset, int count){ +// endMark.setVisibility(View.GONE); AccountSessionManager.getInstance() .getAccount(accountID).getCacheController() .getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing, new SimpleCallback<>(this){ @Override - public void onSuccess(CacheablePaginatedResponse> result){ + public void onSuccess(PaginatedResponse> result){ + if(getActivity()==null) + return; + onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty()); + maxID=result.maxID; +// endMark.setVisibility(result.items.isEmpty() ? View.VISIBLE : View.GONE); + } + }); + } + + /* protected void oldDoLoadData(int offset, int count){ + AccountSessionManager.getInstance() + .getAccount(accountID).getCacheController() + .getNotifications(offset>0 ? maxID : null, count, onlyMentions, onlyPosts, refreshing, new SimpleCallback<>(this){ + @Override + public void onSuccess(PaginatedResponse> result){ if (getActivity() == null) return; if(refreshing) relationships.clear(); @@ -156,21 +167,22 @@ public void onSuccess(CacheablePaginatedResponse> result){ loadRelationships(needRelationships); maxID=result.maxID; - Markers markers = AccountSessionManager.getInstance().getAccount(accountID).markers; - if(offset==0 && !result.items.isEmpty() && !result.isFromCache() && markers != null && markers.notifications != null){ - E.post(new AllNotificationsSeenEvent()); - new SaveMarkers(null, result.items.get(0).id).exec(accountID); - AccountSessionManager.getInstance().getAccount(accountID).markers - .notifications.lastReadId = result.items.get(0).id; - AccountSessionManager.getInstance().writeAccountsFile(); - - if (isInstanceAkkoma()) { - new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID); - } - } + // TODO +// TimelineMarkers markers = AccountSessionManager.getInstance().getAccount(accountID).markers; +// if(offset==0 && !result.items.isEmpty() && !result.isFromCache() && markers != null && markers.notifications != null){ +// E.post(new AllNotificationsSeenEvent()); +// new SaveMarkers(null, result.items.get(0).id).exec(accountID); +// AccountSessionManager.getInstance().getAccount(accountID).markers +// .notifications.lastReadId = result.items.get(0).id; +// AccountSessionManager.getInstance().writeAccountsFile(); +// +// if (isInstanceAkkoma()) { +// new PleromaMarkNotificationsRead(result.items.get(0).id).exec(accountID); +// } +// } } }); - } + } */ @Override protected void onRelationshipsLoaded(){ @@ -186,6 +198,7 @@ protected void onRelationshipsLoaded(){ @Override protected void onShown(){ super.onShown(); + unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker(); // if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) // loadData(); } @@ -293,4 +306,30 @@ public Uri getWebUri(Uri.Builder base) { ? "/users/" + getSession().self.username + "/interactions" : "/notifications").build(); } + + List getData() { + return data; + } + + String getRealUnreadMarker() { + return realUnreadMarker; + } + + void setRealUnreadMarker(String realUnreadMarker) { + this.realUnreadMarker = realUnreadMarker; + } + + @Override + public void onAppendItems(List items){ + super.onAppendItems(items); + if(data.isEmpty() || data.get(0).id.equals(realUnreadMarker)) + return; + for(Notification n:items){ + if(ObjectIdComparator.INSTANCE.compare(n.id, realUnreadMarker)<=0 + && getParentFragment() instanceof NotificationsFragment p){ + p.markAsRead(); + break; + } + } + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/PinnedPostsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/PinnedPostsListFragment.java index d0fa426e60..53c44d36d8 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/PinnedPostsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/PinnedPostsListFragment.java @@ -6,7 +6,6 @@ import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses; import org.joinmastodon.android.model.Account; -import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Status; import org.parceler.Parcels; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/ScheduledStatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/ScheduledStatusListFragment.java index d882bb16ff..ba02fc304d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/ScheduledStatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/ScheduledStatusListFragment.java @@ -80,7 +80,7 @@ public void onViewCreated(View view, Bundle savedInstanceState) { @Override protected List buildDisplayItems(ScheduledStatus s) { - return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, null, true, null); + return StatusDisplayItem.buildItems(this, s.toStatus(), accountID, s, knownAccounts, false, false, true, null); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusEditHistoryFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusEditHistoryFragment.java index 0fb821080b..32dcd2e577 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusEditHistoryFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusEditHistoryFragment.java @@ -7,7 +7,6 @@ import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.statuses.GetStatusEditHistory; -import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem; @@ -58,7 +57,7 @@ public void onSuccess(List result){ @Override protected List buildDisplayItems(Status s){ - List items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, null, StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET); + List items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET); int idx=data.indexOf(s); if(idx>=0){ String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault())); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java index 5b029bc7c6..61910d6426 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/StatusListFragment.java @@ -1,6 +1,5 @@ package org.joinmastodon.android.fragments; -import android.app.assist.AssistContent; import android.content.res.Configuration; import android.os.Bundle; @@ -14,7 +13,6 @@ import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.events.StatusDeletedEvent; import org.joinmastodon.android.events.StatusUpdatedEvent; -import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem; @@ -36,7 +34,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment protected List buildDisplayItems(Status s){ boolean addFooter = !GlobalUserPreferences.spectatorMode || (this instanceof ThreadFragment t && s.id.equals(t.mainStatus.id)); - return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, getFilterContext(), addFooter ? 0 : StatusDisplayItem.FLAG_NO_FOOTER); + return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), addFooter ? 0 : StatusDisplayItem.FLAG_NO_FOOTER); } protected abstract FilterContext getFilterContext(); diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/BubbleTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/BubbleTimelineFragment.java index 75899a177c..450062837f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/BubbleTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/BubbleTimelineFragment.java @@ -5,9 +5,7 @@ import android.view.View; import org.joinmastodon.android.api.requests.timelines.GetBubbleTimeline; -import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline; import org.joinmastodon.android.fragments.StatusListFragment; -import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverPostsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverPostsFragment.java index 60d73e387d..b1773081ef 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverPostsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/DiscoverPostsFragment.java @@ -6,7 +6,6 @@ import org.joinmastodon.android.api.requests.trends.GetTrendingStatuses; import org.joinmastodon.android.fragments.StatusListFragment; -import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/FederatedTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/FederatedTimelineFragment.java index 440b3d3320..54df489d98 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/FederatedTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/FederatedTimelineFragment.java @@ -4,10 +4,8 @@ import android.os.Bundle; import android.view.View; -import org.joinmastodon.android.R; import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline; import org.joinmastodon.android.fragments.StatusListFragment; -import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java index 080a3aa0aa..627be331bd 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/LocalTimelineFragment.java @@ -5,9 +5,7 @@ import android.view.View; import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline; -import org.joinmastodon.android.api.session.AccountSessionManager; import org.joinmastodon.android.fragments.StatusListFragment; -import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper; diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java index 96e1a74347..8ae4d2294c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/discover/SearchFragment.java @@ -15,7 +15,6 @@ import org.joinmastodon.android.fragments.ProfileFragment; import org.joinmastodon.android.fragments.ThreadFragment; import org.joinmastodon.android.model.Account; -import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Hashtag; import org.joinmastodon.android.model.SearchResult; @@ -82,7 +81,7 @@ protected List buildDisplayItems(SearchResult s){ return switch(s.type){ case ACCOUNT -> Collections.singletonList(new AccountStatusDisplayItem(s.id, this, s.account)); case HASHTAG -> Collections.singletonList(new HashtagStatusDisplayItem(s.id, this, s.hashtag)); - case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, null, FilterContext.PUBLIC, 0); + case STATUS -> StatusDisplayItem.buildItems(this, s.status, accountID, s, knownAccounts, FilterContext.PUBLIC, 0); }; } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java index 9cff06fb05..75c80c5e65 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/report/ReportAddPostsChoiceFragment.java @@ -22,7 +22,6 @@ import org.joinmastodon.android.events.FinishReportFragmentsEvent; import org.joinmastodon.android.fragments.StatusListFragment; import org.joinmastodon.android.model.Account; -import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem; diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/FilterResult.java b/mastodon/src/main/java/org/joinmastodon/android/model/FilterResult.java index ec166a572c..fd5088bbc5 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/FilterResult.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/FilterResult.java @@ -7,7 +7,7 @@ @Parcel public class FilterResult extends BaseModel { - public Filter filter; + public LegacyFilter filter; public List keywordMatches; diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Filter.java b/mastodon/src/main/java/org/joinmastodon/android/model/LegacyFilter.java similarity index 97% rename from mastodon/src/main/java/org/joinmastodon/android/model/Filter.java rename to mastodon/src/main/java/org/joinmastodon/android/model/LegacyFilter.java index 12196c0358..123b16356f 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Filter.java +++ b/mastodon/src/main/java/org/joinmastodon/android/model/LegacyFilter.java @@ -13,7 +13,7 @@ import java.util.regex.Pattern; @Parcel -public class Filter extends BaseModel{ +public class LegacyFilter extends BaseModel{ public String id; public String phrase; public String title; diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/Markers.java b/mastodon/src/main/java/org/joinmastodon/android/model/Markers.java deleted file mode 100644 index 6c07cdce15..0000000000 --- a/mastodon/src/main/java/org/joinmastodon/android/model/Markers.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.joinmastodon.android.model; - -public class Markers { - public Marker notifications; - public Marker home; - - @Override - public String toString() { - return "Markers{" + - "notifications=" + notifications + - ", home=" + home + - '}'; - } -} diff --git a/mastodon/src/main/java/org/joinmastodon/android/model/TimelineMarkers.java b/mastodon/src/main/java/org/joinmastodon/android/model/TimelineMarkers.java new file mode 100644 index 0000000000..aabe6df550 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/model/TimelineMarkers.java @@ -0,0 +1,13 @@ +package org.joinmastodon.android.model; + +public class TimelineMarkers{ + public Marker home, notifications; + + @Override + public String toString(){ + return "TimelineMarkers{"+ + "home="+home+ + ", notifications="+notifications+ + '}'; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java index 3fd1ae972d..e4c8dba11b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/HeaderStatusDisplayItem.java @@ -105,6 +105,7 @@ public HeaderStatusDisplayItem(String parentID, Account user, Instant createdAt, } this.extraText=extraText; emojiHelper.addText(extraText); +// this.needBottomPadding = true; } public static HeaderStatusDisplayItem fromAnnouncement(Announcement a, Status fakeStatus, Account instanceUser, BaseStatusListFragment parentFragment, String accountID, Consumer consumeReadID) { @@ -324,11 +325,12 @@ else if (item.status != null && item.status.editedAt != null) timestamp.setText(""); } deleteNotification.setVisibility(GlobalUserPreferences.enableDeleteNotifications && item.notification!=null && !item.inset ? View.VISIBLE : View.GONE); - if (item.hasVisibilityToggle && !item.inset){ - boolean disabled = !TextUtils.isEmpty(item.status.spoilerText) && !item.status.spoilerRevealed; + if (item.hasVisibilityToggle){ + boolean disabled = !item.status.sensitiveRevealed || + (!TextUtils.isEmpty(item.status.spoilerText) && + !item.status.spoilerRevealed); visibility.setEnabled(!disabled); V.setVisibilityAnimated(visibility, disabled ? View.INVISIBLE : View.VISIBLE); - visibility.setImageResource(item.status.sensitiveRevealed ? R.drawable.ic_fluent_eye_off_24_regular : R.drawable.ic_fluent_eye_24_regular); visibility.setContentDescription(item.parentFragment.getString(item.status.sensitiveRevealed ? R.string.hide_content : R.string.reveal_content)); if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){ visibility.setTooltipText(visibility.getContentDescription()); diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java index 51bd9a9a9c..05bd38e625 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/MediaGridStatusDisplayItem.java @@ -152,7 +152,7 @@ public Holder(Activity activity, ViewGroup parent){ @Override public void onBind(MediaGridStatusDisplayItem item){ - wrapper.setPadding(0, 0, 0, 0); // item.inset ? 0 : V.dp(8)); +// wrapper.setPadding(0, 0, 0, item.inset ? 0 : V.dp(8)); if(altTextAnimator!=null) altTextAnimator.cancel(); @@ -208,6 +208,8 @@ else if (!item.status.sensitive) sensitiveText.setText(R.string.media_hidden); else sensitiveText.setText(R.string.sensitive_content_explain); + + applyRequestedTopMargin(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/NotificationHeaderStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/NotificationHeaderStatusDisplayItem.java new file mode 100644 index 0000000000..81cc06ec41 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/NotificationHeaderStatusDisplayItem.java @@ -0,0 +1,175 @@ +package org.joinmastodon.android.ui.displayitems; + +import static org.joinmastodon.android.MastodonApp.context; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.res.ColorStateList; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.text.SpannableStringBuilder; +import android.text.TextUtils; +import android.text.style.TypefaceSpan; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import org.joinmastodon.android.GlobalUserPreferences; +import org.joinmastodon.android.R; +import org.joinmastodon.android.fragments.BaseStatusListFragment; +import org.joinmastodon.android.fragments.ProfileFragment; +import org.joinmastodon.android.model.Notification; +import org.joinmastodon.android.ui.OutlineProviders; +import org.joinmastodon.android.ui.text.HtmlParser; +import org.joinmastodon.android.ui.utils.CustomEmojiHelper; +import org.joinmastodon.android.ui.utils.UiUtils; +import org.parceler.Parcels; + +import me.grishka.appkit.Nav; +import me.grishka.appkit.imageloader.ImageLoaderViewHolder; +import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; +import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest; +import me.grishka.appkit.utils.V; + +public class NotificationHeaderStatusDisplayItem extends StatusDisplayItem{ + public final Notification notification; + private ImageLoaderRequest avaRequest; + private String accountID; + private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); + private CharSequence text; + + public NotificationHeaderStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Notification notification, String accountID){ + super(parentID, parentFragment); + this.notification=notification; + this.accountID=accountID; + + if(notification.type==Notification.Type.POLL){ + text=parentFragment.getString(R.string.poll_ended); + }else{ + avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? notification.account.avatar : notification.account.avatarStatic, V.dp(50), V.dp(50)); + SpannableStringBuilder parsedName=new SpannableStringBuilder(notification.account.displayName); + HtmlParser.parseCustomEmoji(parsedName, notification.account.emojis); + emojiHelper.setText(parsedName); + String[] parts=parentFragment.getString(switch(notification.type){ + case FOLLOW -> R.string.user_followed_you; + case FOLLOW_REQUEST -> R.string.user_sent_follow_request; + case REBLOG -> R.string.notification_boosted; + case FAVORITE -> R.string.user_favorited; + case POLL -> R.string.poll_ended; + case UPDATE -> R.string.sk_post_edited; + case SIGN_UP -> R.string.sk_signed_up; + case REPORT -> R.string.sk_reported; + case REACTION, PLEROMA_EMOJI_REACTION -> + notification.emoji != null ? R.string.sk_reacted_with : R.string.sk_reacted; + default -> throw new IllegalStateException("Unexpected value: "+notification.type); + }).split("%s", 4); + SpannableStringBuilder text=new SpannableStringBuilder(); + if(parts.length>1 && !TextUtils.isEmpty(parts[0])) + text.append(parts[0]); + text.append(parsedName, new TypefaceSpan("sans-serif-medium"), 0); + + if(parts.length==1){ + text.append(parts[0]); + }else if(!TextUtils.isEmpty(parts[1]) && parts.length < 3){ + text.append(parts[1]); + } else if (parts.length == 3 && notification.emoji != null) { + text.append(parts[1]).append(notification.emoji); + } + this.text=text; + } + } + + @Override + public Type getType(){ + return Type.NOTIFICATION_HEADER; + } + + @Override + public int getImageCount(){ + return 1+emojiHelper.getImageCount(); + } + + @Override + public ImageLoaderRequest getImageRequest(int index){ + if(index>0){ + return emojiHelper.getImageRequest(index-1); + } + return avaRequest; + } + + public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ + private final ImageView icon, avatar; + private final TextView text; + private final int selectableItemBackground; + + public Holder(Activity activity, ViewGroup parent){ + super(activity, R.layout.display_item_notification_header, parent); + icon=findViewById(R.id.icon); + avatar=findViewById(R.id.avatar); + text=findViewById(R.id.text); + + avatar.setOutlineProvider(OutlineProviders.roundedRect(8)); + avatar.setClipToOutline(true); + + itemView.setOnClickListener(this::onItemClick); + TypedValue outValue = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true); + selectableItemBackground = outValue.resourceId; + } + + @Override + public void setImage(int index, Drawable image){ + if(index==0){ + avatar.setImageDrawable(image); + }else{ + item.emojiHelper.setImageDrawable(index-1, image); + text.invalidate(); + } + } + + @Override + public void clearImage(int index){ + if(index==0) + avatar.setImageResource(R.drawable.image_placeholder); + else + ImageLoaderViewHolder.super.clearImage(index); + } + + @SuppressLint("ResourceType") + @Override + public void onBind(NotificationHeaderStatusDisplayItem item){ + text.setText(item.text); + avatar.setVisibility(item.notification.type==Notification.Type.POLL ? View.GONE : View.VISIBLE); + icon.setImageResource(switch(item.notification.type){ + case FAVORITE -> R.drawable.ic_fluent_star_24_filled; + case REBLOG -> R.drawable.ic_fluent_arrow_repeat_all_24_filled; + case FOLLOW, FOLLOW_REQUEST -> R.drawable.ic_fluent_person_add_24_filled; + case POLL -> R.drawable.ic_fluent_poll_24_filled; + case REPORT -> R.drawable.ic_fluent_warning_24_filled; + case SIGN_UP -> R.drawable.ic_fluent_person_available_24_filled; + case UPDATE -> R.drawable.ic_fluent_edit_24_filled; + case REACTION, PLEROMA_EMOJI_REACTION -> R.drawable.ic_fluent_add_24_filled; + default -> throw new IllegalStateException("Unexpected value: "+item.notification.type); + }); + icon.setImageTintList(ColorStateList.valueOf(UiUtils.getThemeColor(item.parentFragment.getActivity(), switch(item.notification.type){ + case FAVORITE -> R.attr.colorFavorite; + case REBLOG -> R.attr.colorBoost; + case FOLLOW, FOLLOW_REQUEST -> R.attr.colorFollow; + case POLL -> R.attr.colorPoll; + default -> android.R.attr.colorAccent; + }))); + itemView.setBackgroundResource(item.notification.type != Notification.Type.POLL ? + selectableItemBackground : 0); + itemView.setClickable(item.notification.type != Notification.Type.POLL); + } + + public void onItemClick(View v) { + Bundle args=new Bundle(); + args.putString("account", item.accountID); + args.putParcelable("profileAccount", Parcels.wrap(item.notification.account)); + Nav.go(item.parentFragment.getActivity(), ProfileFragment.class, args); + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java index b01b781482..3495d1714d 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/PollOptionStatusDisplayItem.java @@ -5,6 +5,7 @@ import android.graphics.drawable.Drawable; import android.view.View; import android.view.ViewGroup; +import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; @@ -96,7 +97,7 @@ public void onBind(PollOptionStatusDisplayItem item){ Drawable bg=item.inset ? progressBgInset : progressBg; bg.setLevel(Math.round(10000f*item.votesFraction)); button.setBackground(bg); - itemView.setSelected(item.isMostVoted); + itemView.setSelected(item.poll.ownVotes.contains(item.optionIndex)); percent.setText(String.format(Locale.getDefault(), "%d%%", Math.round(item.votesFraction*100f))); }else{ itemView.setSelected(item.poll.selectedOptions!=null && item.poll.selectedOptions.contains(item.option)); @@ -109,6 +110,8 @@ public void onBind(PollOptionStatusDisplayItem item){ text.setTextColor(UiUtils.getThemeColor(itemView.getContext(), android.R.attr.textColorPrimary)); percent.setTextColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3OnSecondaryContainer)); } + + applyRequestedTopMargin(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/SpoilerStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/SpoilerStatusDisplayItem.java index 45013415e8..df88fc2d0e 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/SpoilerStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/SpoilerStatusDisplayItem.java @@ -21,6 +21,7 @@ import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; +import me.grishka.appkit.utils.V; public class SpoilerStatusDisplayItem extends StatusDisplayItem{ public final Status status; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java index 1622dbc071..a4a91d2b4a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/StatusDisplayItem.java @@ -20,7 +20,7 @@ import org.joinmastodon.android.model.Account; import org.joinmastodon.android.model.Attachment; import org.joinmastodon.android.model.DisplayItemsParent; -import org.joinmastodon.android.model.Filter; +import org.joinmastodon.android.model.LegacyFilter; import org.joinmastodon.android.model.FilterAction; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.FilterResult; @@ -42,6 +42,7 @@ import me.grishka.appkit.Nav; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; import me.grishka.appkit.utils.BindableViewHolder; +import me.grishka.appkit.utils.V; import me.grishka.appkit.views.UsableRecyclerView; public abstract class StatusDisplayItem{ @@ -61,6 +62,7 @@ public abstract class StatusDisplayItem{ public static final int FLAG_MEDIA_FORCE_HIDDEN=1 << 3; public static final int FLAG_NO_HEADER=1 << 4; public static final int FLAG_NO_TRANSLATE=1 << 5; + boolean needTopPadding; public void setAncestryInfo( boolean hasDescendantNeighbor, @@ -110,12 +112,12 @@ public static BindableViewHolder createViewHolder(T case FILE -> new FileStatusDisplayItem.Holder(activity, parent); case SPOILER, FILTER_SPOILER -> new SpoilerStatusDisplayItem.Holder(activity, parent, type); case SECTION_HEADER -> null; // new SectionHeaderStatusDisplayItem.Holder(activity, parent); - case NOTIFICATION_HEADER -> null; // new NotificationHeaderStatusDisplayItem.Holder(activity, parent); + case NOTIFICATION_HEADER -> new NotificationHeaderStatusDisplayItem.Holder(activity, parent); case DUMMY -> new InsetDummyStatusDisplayItem.Holder(activity); }; } - public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts, boolean inset, boolean addFooter, Notification notification, boolean disableTranslate, FilterContext filterContext) { + public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts, boolean inset, boolean addFooter, boolean disableTranslate, FilterContext filterContext) { int flags=0; if(inset) flags|=FLAG_INSET; @@ -123,7 +125,7 @@ public static ArrayList buildItems(BaseStatusListFragment flags|=FLAG_NO_FOOTER; if (disableTranslate) flags|=FLAG_NO_TRANSLATE; - return buildItems(fragment, status, accountID, parentObject, knownAccounts, null, filterContext, flags); + return buildItems(fragment, status, accountID, parentObject, knownAccounts, filterContext, flags); } public static ReblogOrReplyLineStatusDisplayItem buildReplyLine(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parent, Account account, boolean threadReply) { @@ -141,7 +143,7 @@ public static ReblogOrReplyLineStatusDisplayItem buildReplyLine(BaseStatusListFr ); } - public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts, Notification notification, FilterContext filterContext, int flags){ + public static ArrayList buildItems(BaseStatusListFragment fragment, Status status, String accountID, DisplayItemsParent parentObject, Map knownAccounts, FilterContext filterContext, int flags){ String parentID=parentObject.getID(); ArrayList items=new ArrayList<>(); Status statusForContent=status.getContentStatus(); @@ -205,7 +207,7 @@ public static ArrayList buildItems(BaseStatusListFragment if((flags & FLAG_CHECKABLE)!=0) items.add(header=new CheckableHeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null)); else - items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, notification, scheduledStatus)); + items.add(header=new HeaderStatusDisplayItem(parentID, statusForContent.account, statusForContent.createdAt, fragment, accountID, statusForContent, null, null, scheduledStatus)); } boolean filtered=false; @@ -218,10 +220,11 @@ public static ArrayList buildItems(BaseStatusListFragment } } + SpoilerStatusDisplayItem spoilerItem; ArrayList contentItems; if(!TextUtils.isEmpty(statusForContent.spoilerText)){ if (GlobalUserPreferences.alwaysExpandContentWarnings) statusForContent.spoilerRevealed = true; - SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER); + spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER); items.add(spoilerItem); contentItems=spoilerItem.contentItems; }else{ @@ -238,13 +241,14 @@ public static ArrayList buildItems(BaseStatusListFragment } } + int indexWhereTheTextWouldBe = -1; if(!TextUtils.isEmpty(statusForContent.content)){ SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID); HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered); TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0); contentItems.add(text); - } else if (header!=null){ - header.needBottomPadding=true; + } else { + indexWhereTheTextWouldBe = contentItems.size(); } List imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList()); @@ -271,6 +275,15 @@ else if(statusForContent.sensitive && GlobalUserPreferences.alwaysExpandContentW if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty()){ contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent)); } + + if (TextUtils.isEmpty(statusForContent.content) && contentItems.indexOf(header) != contentItems.size() - 1) { + header.needBottomPadding = false; + } + // adding back the padding that the text status display item usually provides + if (indexWhereTheTextWouldBe >= 0 && contentItems.size() > indexWhereTheTextWouldBe) { + contentItems.get(indexWhereTheTextWouldBe).needTopPadding = true; + } + if(contentItems!=items && statusForContent.spoilerRevealed){ items.addAll(contentItems); } @@ -290,7 +303,7 @@ else if(statusForContent.sensitive && GlobalUserPreferences.alwaysExpandContentW item.index=i++; } // add dummy item that provides the missing top margin. workarounds, huh - if (inset) items.add(0, new InsetDummyStatusDisplayItem(parentID, fragment, true)); +// if (inset) items.add(0, new InsetDummyStatusDisplayItem(parentID, fragment, true)); if(items!=contentItems && !statusForContent.spoilerRevealed){ for(StatusDisplayItem item:contentItems){ item.inset=inset; @@ -298,7 +311,7 @@ else if(statusForContent.sensitive && GlobalUserPreferences.alwaysExpandContentW } } - Filter applyingFilter = null; + LegacyFilter applyingFilter = null; if (!statusForContent.filterRevealed) { StatusFilterPredicate predicate = new StatusFilterPredicate(accountID, filterContext, FilterAction.WARN); statusForContent.filterRevealed = predicate.test(status); @@ -365,5 +378,15 @@ public void onClick(){ public boolean isEnabled(){ return item.parentFragment.isItemEnabled(item.parentID); } + + protected void applyRequestedTopMargin(ViewGroup.MarginLayoutParams params) { + applyRequestedTopMargin(params, 0); + } + + protected void applyRequestedTopMargin(ViewGroup.MarginLayoutParams params, int defaultValue) { + params.setMargins(params.leftMargin, item.needTopPadding ? V.dp(16) : defaultValue, + params.rightMargin, params.bottomMargin); + itemView.setLayoutParams(params); + } } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/WarningFilteredStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/WarningFilteredStatusDisplayItem.java index 932a9bf106..1640572c5b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/WarningFilteredStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/WarningFilteredStatusDisplayItem.java @@ -7,7 +7,7 @@ import org.joinmastodon.android.R; import org.joinmastodon.android.fragments.BaseStatusListFragment; -import org.joinmastodon.android.model.Filter; +import org.joinmastodon.android.model.LegacyFilter; import org.joinmastodon.android.model.Status; import java.util.List; @@ -16,9 +16,9 @@ public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{ public boolean loading; public final Status status; public List filteredItems; - public Filter applyingFilter; + public LegacyFilter applyingFilter; - public WarningFilteredStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status, List filteredItems, Filter applyingFilter){ + public WarningFilteredStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status, List filteredItems, LegacyFilter applyingFilter){ super(parentID, parentFragment); this.status=status; this.filteredItems = filteredItems; diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/CheckIconSelectableTextView.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/CheckIconSelectableTextView.java new file mode 100644 index 0000000000..7b21933b9e --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/CheckIconSelectableTextView.java @@ -0,0 +1,39 @@ +package org.joinmastodon.android.ui.views; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.TextView; + +import org.joinmastodon.android.R; +import org.joinmastodon.android.ui.utils.UiUtils; + +public class CheckIconSelectableTextView extends TextView{ + + private boolean currentlySelected; + + public CheckIconSelectableTextView(Context context){ + this(context, null); + } + + public CheckIconSelectableTextView(Context context, AttributeSet attrs){ + this(context, attrs, 0); + } + + public CheckIconSelectableTextView(Context context, AttributeSet attrs, int defStyle){ + super(context, attrs, defStyle); + } + + @Override + protected void drawableStateChanged(){ + super.drawableStateChanged(); + if(currentlySelected==isSelected()) + return; + currentlySelected=isSelected(); + Drawable start=currentlySelected ? getResources().getDrawable(R.drawable.ic_baseline_check_18, getContext().getTheme()).mutate() : null; + if(start!=null) + start.setTint(UiUtils.getThemeColor(getContext(), R.attr.colorM3OnSurface)); + Drawable end=getCompoundDrawablesRelative()[2]; + setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/NestedRecyclerScrollView.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/NestedRecyclerScrollView.java index 8bad60d6e4..bdcd31828c 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/views/NestedRecyclerScrollView.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/NestedRecyclerScrollView.java @@ -1,8 +1,13 @@ package org.joinmastodon.android.ui.views; import android.content.Context; +import android.content.res.TypedArray; import android.util.AttributeSet; import android.view.View; +import android.webkit.WebView; +import android.widget.ScrollView; + +import org.joinmastodon.android.R; import java.util.function.Supplier; @@ -10,63 +15,102 @@ import androidx.recyclerview.widget.RecyclerView; public class NestedRecyclerScrollView extends CustomScrollView{ - private Supplier scrollableChildSupplier; + private Supplier scrollableChildSupplier; + private boolean takePriorityOverChildViews; public NestedRecyclerScrollView(Context context){ - super(context); + this(context, null); } public NestedRecyclerScrollView(Context context, AttributeSet attrs){ - super(context, attrs); + this(context, attrs, 0); } - public NestedRecyclerScrollView(Context context, AttributeSet attrs, int defStyleAttr){ - super(context, attrs, defStyleAttr); + public NestedRecyclerScrollView(Context context, AttributeSet attrs, int defStyle){ + super(context, attrs, defStyle); + TypedArray ta=context.obtainStyledAttributes(attrs, R.styleable.NestedRecyclerScrollView); + takePriorityOverChildViews=ta.getBoolean(R.styleable.NestedRecyclerScrollView_takePriorityOverChildViews, false); + ta.recycle(); } @Override - public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { - final RecyclerView rv = (RecyclerView) target; - if ((dy < 0 && isScrolledToTop(rv)) || (dy > 0 && !isScrolledToBottom())) { + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed){ + if(takePriorityOverChildViews){ + if((dy<0 && getScrollY()>0) || (dy>0 && canScrollVertically(1))){ + scrollBy(0, dy); + consumed[1]=dy; + return; + } + }else if((dy<0 && isScrolledToTop(target)) || (dy>0 && !isScrolledToBottom())){ scrollBy(0, dy); - consumed[1] = dy; + consumed[1]=dy; return; } super.onNestedPreScroll(target, dx, dy, consumed); } @Override - public boolean onNestedPreFling(View target, float velX, float velY) { - final RecyclerView rv = (RecyclerView) target; - if ((velY < 0 && isScrolledToTop(rv)) || (velY > 0 && !isScrolledToBottom())) { + public boolean onNestedPreFling(View target, float velX, float velY){ + if(takePriorityOverChildViews){ + if((velY<0 && getScrollY()>0) || (velY>0 && canScrollVertically(1))){ + fling((int)velY); + return true; + } + }else if((velY<0 && isScrolledToTop(target)) || (velY>0 && !isScrolledToBottom())){ fling((int) velY); return true; } return super.onNestedPreFling(target, velX, velY); } - private boolean isScrolledToBottom() { + private boolean isScrolledToBottom(){ return !canScrollVertically(1); } - private boolean isScrolledToTop(RecyclerView rv) { - final LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager(); - return lm.findFirstVisibleItemPosition() == 0 - && lm.findViewByPosition(0).getTop() == rv.getPaddingTop(); + private boolean isScrolledToTop(View view){ + if(view instanceof RecyclerView rv){ + final LinearLayoutManager lm=(LinearLayoutManager) rv.getLayoutManager(); + return lm.findFirstVisibleItemPosition()==0 + && lm.findViewByPosition(0).getTop()==rv.getPaddingTop(); + } + return !view.canScrollVertically(-1); } - public void setScrollableChildSupplier(Supplier scrollableChildSupplier){ + public void setScrollableChildSupplier(Supplier scrollableChildSupplier){ this.scrollableChildSupplier=scrollableChildSupplier; } @Override protected boolean onScrollingHitEdge(float velocity){ - if(velocity>0){ - RecyclerView view=scrollableChildSupplier.get(); - if(view!=null){ - return view.fling(0, (int)velocity); + if(velocity>0 || takePriorityOverChildViews){ + View view=scrollableChildSupplier==null ? null : scrollableChildSupplier.get(); + if(view instanceof RecyclerView rv){ + return rv.fling(0, (int) velocity); + }else if(view instanceof ScrollView sv){ + if(sv.canScrollVertically((int)velocity)){ + sv.fling((int)velocity); + return true; + } + }else if(view instanceof CustomScrollView sv){ + if(sv.canScrollVertically((int)velocity)){ + sv.fling((int)velocity); + return true; + } + }else if(view instanceof WebView wv){ + if(wv.canScrollVertically((int)velocity)){ + wv.flingScroll(0, (int)velocity); + return true; + } } } return false; } + + public boolean isTakePriorityOverChildViews(){ + return takePriorityOverChildViews; + } + + public void setTakePriorityOverChildViews(boolean takePriorityOverChildViews){ + this.takePriorityOverChildViews=takePriorityOverChildViews; + } } diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/views/TopBarsScrollAwayLinearLayout.java b/mastodon/src/main/java/org/joinmastodon/android/ui/views/TopBarsScrollAwayLinearLayout.java new file mode 100644 index 0000000000..975f6a27ab --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/views/TopBarsScrollAwayLinearLayout.java @@ -0,0 +1,29 @@ +package org.joinmastodon.android.ui.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.LinearLayout; + +public class TopBarsScrollAwayLinearLayout extends LinearLayout{ + public TopBarsScrollAwayLinearLayout(Context context){ + this(context, null); + } + + public TopBarsScrollAwayLinearLayout(Context context, AttributeSet attrs){ + this(context, attrs, 0); + } + + public TopBarsScrollAwayLinearLayout(Context context, AttributeSet attrs, int defStyle){ + super(context, attrs, defStyle); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){ + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + int topBarsHeight=0; + for(int i=0;i{ + public static final ObjectIdComparator INSTANCE=new ObjectIdComparator(); + + @Override + public int compare(String o1, String o2){ + int l1=o1==null ? 0 : o1.length(); + int l2=o2==null ? 0 : o2.length(); + if(l1!=l2) + return Integer.compare(l1, l2); + if(l1==0) + return 0; + return o1.compareTo(o2); + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/utils/StatusFilterPredicate.java b/mastodon/src/main/java/org/joinmastodon/android/utils/StatusFilterPredicate.java index fd1b957541..814c9f8f92 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/utils/StatusFilterPredicate.java +++ b/mastodon/src/main/java/org/joinmastodon/android/utils/StatusFilterPredicate.java @@ -1,7 +1,7 @@ package org.joinmastodon.android.utils; import org.joinmastodon.android.api.session.AccountSessionManager; -import org.joinmastodon.android.model.Filter; +import org.joinmastodon.android.model.LegacyFilter; import org.joinmastodon.android.model.FilterAction; import org.joinmastodon.android.model.FilterContext; import org.joinmastodon.android.model.Status; @@ -14,23 +14,23 @@ import java.util.stream.Stream; public class StatusFilterPredicate implements Predicate{ - private final List filters; + private final List filters; private final FilterContext context; private final FilterAction action; - private Filter applyingFilter; + private LegacyFilter applyingFilter; /** * @param context null makes the predicate pass automatically * @param action defines what the predicate should check: * status should not be hidden or should not display with warning */ - public StatusFilterPredicate(List filters, FilterContext context, FilterAction action){ + public StatusFilterPredicate(List filters, FilterContext context, FilterAction action){ this.filters = filters; this.context = context; this.action = action; } - public StatusFilterPredicate(List filters, FilterContext context){ + public StatusFilterPredicate(List filters, FilterContext context){ this(filters, context, FilterAction.HIDE); } @@ -62,13 +62,13 @@ public StatusFilterPredicate(String accountID, FilterContext context){ public boolean test(Status status){ if (context == null) return true; - Stream matchingFilters = status.filtered != null + Stream matchingFilters = status.filtered != null // use server-provided per-status info (status.filtered) if available ? status.filtered.stream().map(f -> f.filter) // or fall back to cached filters : filters.stream().filter(filter -> filter.matches(status)); - Optional applyingFilter = matchingFilters + Optional applyingFilter = matchingFilters // discard expired filters .filter(filter -> filter.expiresAt == null || filter.expiresAt.isAfter(Instant.now())) // only apply filters for given context @@ -81,7 +81,7 @@ public boolean test(Status status){ return applyingFilter.isEmpty(); } - public Filter getApplyingFilter() { + public LegacyFilter getApplyingFilter() { return applyingFilter; } } diff --git a/mastodon/src/main/res/color/text_segmented_button.xml b/mastodon/src/main/res/color/text_segmented_button.xml new file mode 100644 index 0000000000..4f7f4bd866 --- /dev/null +++ b/mastodon/src/main/res/color/text_segmented_button.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_segmented_button.xml b/mastodon/src/main/res/drawable/bg_segmented_button.xml new file mode 100644 index 0000000000..f6b62e4334 --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_segmented_button.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_tabbar_badge.xml b/mastodon/src/main/res/drawable/bg_tabbar_badge.xml new file mode 100644 index 0000000000..9c784cd0d6 --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_tabbar_badge.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_tabbar_tab.xml b/mastodon/src/main/res/drawable/bg_tabbar_tab.xml new file mode 100644 index 0000000000..4f98d167dd --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_tabbar_tab.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/divider_vertical_outline.xml b/mastodon/src/main/res/drawable/divider_vertical_outline.xml new file mode 100644 index 0000000000..0e355227a3 --- /dev/null +++ b/mastodon/src/main/res/drawable/divider_vertical_outline.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/fg_segmented_button_container.xml b/mastodon/src/main/res/drawable/fg_segmented_button_container.xml new file mode 100644 index 0000000000..c2052ae416 --- /dev/null +++ b/mastodon/src/main/res/drawable/fg_segmented_button_container.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/ic_done_all_24px.xml b/mastodon/src/main/res/drawable/ic_done_all_24px.xml new file mode 100644 index 0000000000..a1ab9b204a --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_done_all_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_add_24_filled.xml b/mastodon/src/main/res/drawable/ic_fluent_add_24_filled.xml new file mode 100644 index 0000000000..cdc307515c --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_add_24_filled.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_fluent_edit_24_filled.xml b/mastodon/src/main/res/drawable/ic_fluent_edit_24_filled.xml new file mode 100644 index 0000000000..b12b33f160 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_fluent_edit_24_filled.xml @@ -0,0 +1,3 @@ + + + diff --git a/mastodon/src/main/res/drawable/image_placeholder.xml b/mastodon/src/main/res/drawable/image_placeholder.xml new file mode 100644 index 0000000000..7cc3cf14d8 --- /dev/null +++ b/mastodon/src/main/res/drawable/image_placeholder.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/display_item_notification_header.xml b/mastodon/src/main/res/layout/display_item_notification_header.xml new file mode 100644 index 0000000000..d9f598b4a5 --- /dev/null +++ b/mastodon/src/main/res/layout/display_item_notification_header.xml @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/tab_bar.xml b/mastodon/src/main/res/layout/tab_bar.xml index 70cd213c3f..420bce6c00 100644 --- a/mastodon/src/main/res/layout/tab_bar.xml +++ b/mastodon/src/main/res/layout/tab_bar.xml @@ -1,5 +1,6 @@ - + + + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:contentDescription="@string/home_timeline"> + + - + - + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:contentDescription="@string/search_hint"> - + - + + + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="1" + android:contentDescription="@string/notifications"> - + + + + + + android:contentDescription="@string/my_profile"> - + diff --git a/mastodon/src/main/res/menu/notifications.xml b/mastodon/src/main/res/menu/notifications.xml index 5ee571b07a..c451bf5c5c 100644 --- a/mastodon/src/main/res/menu/notifications.xml +++ b/mastodon/src/main/res/menu/notifications.xml @@ -6,6 +6,11 @@ android:showAsAction="always" android:visible="false" android:title="@string/sk_follow_requests" /> + + + + + diff --git a/mastodon/src/main/res/values/ids.xml b/mastodon/src/main/res/values/ids.xml index 6994f851b8..f6f17a7f79 100644 --- a/mastodon/src/main/res/values/ids.xml +++ b/mastodon/src/main/res/values/ids.xml @@ -1,6 +1,7 @@ + diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index e0b907038b..6d11d58dd7 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -16,11 +16,11 @@ In reply to %s Notifications - followed you - sent you a follow request - favorited your post - boosted your post - poll ended + %s followed you + %s sent you a follow request + %s favorited your post + %s boosted your post + See the results of a poll you voted in %ds %dm @@ -487,4 +487,5 @@ Downloading (%d%%) Matches filter ā€œ%sā€ + Mark all as read diff --git a/mastodon/src/main/res/values/strings_sk.xml b/mastodon/src/main/res/values/strings_sk.xml index 20190c17a0..c23c22954a 100644 --- a/mastodon/src/main/res/values/strings_sk.xml +++ b/mastodon/src/main/res/values/strings_sk.xml @@ -273,8 +273,8 @@ Enable this if your home instance runs on Glitch. Not needed for Hometown or Akkoma. signed up reported - reacted with %s - reacted + %s reacted with %s + %s reacted Users signing up New reports Server version: %s diff --git a/mastodon/src/main/res/values/styles.xml b/mastodon/src/main/res/values/styles.xml index afabab886a..aefb26976d 100644 --- a/mastodon/src/main/res/values/styles.xml +++ b/mastodon/src/main/res/values/styles.xml @@ -108,6 +108,12 @@ ?colorGray300 ?colorGray50 ?colorGray400 + + @color/favorite_selected + @color/boost_selected + #4746e3 + @color/bookmark_selected + #a6ffffff @@ -211,6 +217,12 @@ ?colorGray300 ?colorGray50 ?colorGray400 + + @color/favorite_selected + @color/boost_selected + #4746e3 + @color/bookmark_selected + #80000000 @@ -457,6 +469,30 @@ ?colorM3OnSurface + + + + + + + +