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 a2eb18c4..ee56c5a3 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/CacheController.java @@ -42,6 +42,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; @@ -131,6 +133,12 @@ public class CacheController{ cancelDelayedClose(); databaseThread.postRunnable(()->{ try{ + if(!onlyMentions && loadingNotifications){ + synchronized(pendingNotificationsCallbacks){ + pendingNotificationsCallbacks.add(callback); + } + return; + } List filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.NOTIFICATIONS)).collect(Collectors.toList()); if(!forceReload){ SQLiteDatabase db=getOrOpenDatabase(); @@ -160,11 +168,13 @@ public class CacheController{ Log.w(TAG, "getNotifications: corrupted notification object in database", x); } } + if(!onlyMentions) + loadingNotifications=true; new GetNotifications(maxID, count, onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class)) .setCallback(new Callback<>(){ @Override public void onSuccess(List result){ - callback.onSuccess(new PaginatedResponse<>(result.stream().filter(ntf->{ + PaginatedResponse> res=new PaginatedResponse<>(result.stream().filter(ntf->{ if(ntf.status!=null){ for(Filter filter:filters){ if(filter.matches(ntf.status)){ @@ -173,13 +183,32 @@ public class CacheController{ } } return true; - }).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id)); + }).collect(Collectors.toList()), result.isEmpty() ? null : result.get(result.size()-1).id); + callback.onSuccess(res); putNotifications(result, onlyMentions, 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/markers/GetMarkers.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/markers/GetMarkers.java new file mode 100644 index 00000000..644665ba --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/markers/GetMarkers.java @@ -0,0 +1,12 @@ +package org.joinmastodon.android.api.requests.markers; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.TimelineMarkers; + +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/requests/markers/SaveMarkers.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/markers/SaveMarkers.java index f432504b..eeda81b4 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/markers/SaveMarkers.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/markers/SaveMarkers.java @@ -2,11 +2,11 @@ package org.joinmastodon.android.api.requests.markers; import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.api.gson.JsonObjectBuilder; -import org.joinmastodon.android.model.Marker; +import org.joinmastodon.android.model.TimelineMarkers; -public class SaveMarkers extends MastodonAPIRequest{ +public class SaveMarkers extends MastodonAPIRequest{ public SaveMarkers(String lastSeenHomePostID, String lastSeenNotificationID){ - super(HttpMethod.POST, "/markers", Response.class); + super(HttpMethod.POST, "/markers", TimelineMarkers.class); JsonObjectBuilder builder=new JsonObjectBuilder(); if(lastSeenHomePostID!=null) builder.add("home", new JsonObjectBuilder().add("last_read_id", lastSeenHomePostID)); @@ -14,8 +14,4 @@ public class SaveMarkers extends MastodonAPIRequest{ builder.add("notifications", new JsonObjectBuilder().add("last_read_id", lastSeenNotificationID)); setRequestBody(builder.build()); } - - public static class Response{ - public Marker home, notifications; - } } 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 cc2868ae..4c4cc03f 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,18 +1,28 @@ package org.joinmastodon.android.api.session; +import android.content.Context; +import android.content.SharedPreferences; +import android.text.TextUtils; import android.util.Log; +import org.joinmastodon.android.E; +import org.joinmastodon.android.MastodonApp; 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.markers.GetMarkers; +import org.joinmastodon.android.api.requests.markers.SaveMarkers; +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.Preferences; import org.joinmastodon.android.model.PushSubscription; +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; @@ -44,6 +54,7 @@ public class AccountSession{ private transient StatusInteractionController statusInteractionController; private transient CacheController cacheController; private transient PushSubscriptionManager pushSubscriptionManager; + private transient SharedPreferences prefs; AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){ this.token=token; @@ -106,4 +117,44 @@ public class AccountSession{ }) .exec(getID()); } + + public SharedPreferences getLocalPreferences(){ + 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 getLocalPreferences().getString("notificationsMarker", null); + } + + public void setNotificationsMarker(String id, boolean clearUnread){ + getLocalPreferences().edit().putString("notificationsMarker", id).apply(); + E.post(new NotificationsMarkerUpdatedEvent(getID(), id, clearUnread)); + } } 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 396e1731..7b553d01 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 @@ -142,6 +142,10 @@ public class AccountSessionManager{ return session; } + public static AccountSession get(String id){ + return getInstance().getAccount(id); + } + @Nullable public AccountSession tryGetAccount(String id){ return sessions.get(id); @@ -174,6 +178,12 @@ public class AccountSessionManager{ AccountSession session=getAccount(id); session.getCacheController().closeDatabase(); MastodonApp.context.deleteDatabase(id+".db"); + MastodonApp.context.getSharedPreferences(id, 0).edit().clear().commit(); + if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){ + MastodonApp.context.deleteSharedPreferences(id); + }else{ + new File(MastodonApp.context.getDir("shared_prefs", Context.MODE_PRIVATE), id+".xml").delete(); + } sessions.remove(id); if(lastActiveAccountID.equals(id)){ if(sessions.isEmpty()) 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 00000000..f68a5b99 --- /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/fragments/HomeFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java index 33a88d7c..7684c571 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeFragment.java @@ -1,10 +1,12 @@ package org.joinmastodon.android.fragments; +import android.annotation.SuppressLint; import android.app.Fragment; import android.app.NotificationManager; import android.graphics.Outline; import android.os.Build; import android.os.Bundle; +import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -14,14 +16,23 @@ import android.view.WindowInsets; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.TextView; +import com.squareup.otto.Subscribe; + +import org.joinmastodon.android.E; import org.joinmastodon.android.PushNotificationReceiver; import org.joinmastodon.android.R; +import org.joinmastodon.android.api.requests.markers.GetMarkers; import org.joinmastodon.android.api.session.AccountSession; import org.joinmastodon.android.api.session.AccountSessionManager; +import org.joinmastodon.android.events.NotificationsMarkerUpdatedEvent; 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.Notification; +import org.joinmastodon.android.model.PaginatedResponse; +import org.joinmastodon.android.model.TimelineMarkers; import org.joinmastodon.android.ui.AccountSwitcherSheet; import org.joinmastodon.android.ui.OutlineProviders; import org.joinmastodon.android.ui.utils.UiUtils; @@ -29,11 +40,14 @@ import org.joinmastodon.android.ui.views.TabBar; import org.parceler.Parcels; import java.util.ArrayList; +import java.util.List; 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; import me.grishka.appkit.fragments.LoaderFragment; import me.grishka.appkit.fragments.OnBackPressedListener; @@ -45,7 +59,7 @@ import me.grishka.appkit.views.FragmentRootLinearLayout; public class HomeFragment extends AppKitFragment implements OnBackPressedListener{ private FragmentRootLinearLayout content; private HomeTimelineFragment homeTimelineFragment; - private NotificationsFragment notificationsFragment; + private NotificationsListFragment notificationsFragment; private DiscoverFragment searchFragment; private ProfileFragment profileFragment; private TabBar tabBar; @@ -53,6 +67,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene private ImageView tabBarAvatar; @IdRes private int currentTab=R.id.tab_home; + private TextView notificationsBadge; private String accountID; @@ -74,7 +89,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene args.putBoolean("noAutoLoad", true); searchFragment=new DiscoverFragment(); searchFragment.setArguments(args); - notificationsFragment=new NotificationsFragment(); + notificationsFragment=new NotificationsListFragment(); notificationsFragment.setArguments(args); args=new Bundle(args); args.putParcelable("profileAccount", Parcels.wrap(AccountSessionManager.getInstance().getAccount(accountID).self)); @@ -83,6 +98,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene profileFragment.setArguments(args); } + E.register(this); + } + + @Override + public void onDestroy(){ + super.onDestroy(); + E.unregister(this); } @Nullable @@ -106,6 +128,9 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene Account self=AccountSessionManager.getInstance().getAccount(accountID).self; ViewImageLoader.loadWithoutAnimation(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24))); + notificationsBadge=tabBar.findViewById(R.id.notifications_badge); + notificationsBadge.setVisibility(View.GONE); + if(savedInstanceState==null){ getChildFragmentManager().beginTransaction() .add(R.id.fragment_wrap, homeTimelineFragment) @@ -138,7 +163,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene return; homeTimelineFragment=(HomeTimelineFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTimelineFragment"); searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment"); - notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment"); + notificationsFragment=(NotificationsListFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment"); profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment"); currentTab=savedInstanceState.getInt("selectedTab"); tabBar.selectTab(currentTab); @@ -224,9 +249,8 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene lf.loadData(); }else if(newFragment instanceof DiscoverFragment){ ((DiscoverFragment) newFragment).loadData(); - }else if(newFragment instanceof NotificationsFragment){ - ((NotificationsFragment) newFragment).loadData(); - // TODO make an interface? + } + if(newFragment instanceof NotificationsListFragment){ NotificationManager nm=getActivity().getSystemService(NotificationManager.class); nm.cancel(accountID, PushNotificationReceiver.NOTIFICATION_ID); } @@ -267,4 +291,62 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene getChildFragmentManager().putFragment(outState, "notificationsFragment", notificationsFragment); getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment); } + + @Override + protected void onShown(){ + super.onShown(); + reloadNotificationsForUnreadCount(); + } + + private void reloadNotificationsForUnreadCount(){ + List[] notifications=new List[]{null}; + String[] marker={null}; + + 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, 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){} + }); + } + + @SuppressLint("DefaultLocale") + private void updateUnreadCount(List notifications, String marker){ + if(notifications.isEmpty() || notifications.get(0).id.compareTo(marker)<=0){ + notificationsBadge.setVisibility(View.GONE); + }else{ + notificationsBadge.setVisibility(View.VISIBLE); + if(notifications.get(notifications.size()-1).id.compareTo(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 onNotificationsMarkerUpdated(NotificationsMarkerUpdatedEvent ev){ + if(!ev.accountID.equals(accountID)) + return; + if(ev.clearUnread) + notificationsBadge.setVisibility(View.GONE); + } } 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 7c9f6d8a..67c88be8 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/HomeTimelineFragment.java @@ -33,6 +33,7 @@ import org.joinmastodon.android.events.StatusCreatedEvent; import org.joinmastodon.android.model.CacheablePaginatedResponse; import org.joinmastodon.android.model.Filter; import org.joinmastodon.android.model.Status; +import org.joinmastodon.android.model.TimelineMarkers; import org.joinmastodon.android.ui.displayitems.GapStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.utils.UiUtils; @@ -154,7 +155,7 @@ public class HomeTimelineFragment extends StatusListFragment{ new SaveMarkers(topPostID, null) .setCallback(new Callback<>(){ @Override - public void onSuccess(SaveMarkers.Response result){ + public void onSuccess(TimelineMarkers result){ } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java deleted file mode 100644 index 1f02f959..00000000 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java +++ /dev/null @@ -1,187 +0,0 @@ -package org.joinmastodon.android.fragments; - -import android.app.Activity; -import android.app.Fragment; -import android.content.res.Configuration; -import android.os.Build; -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; -import android.widget.LinearLayout; - -import org.joinmastodon.android.R; -import org.joinmastodon.android.ui.SimpleViewHolder; -import org.joinmastodon.android.ui.tabs.TabLayout; -import org.joinmastodon.android.ui.tabs.TabLayoutMediator; -import org.joinmastodon.android.ui.utils.UiUtils; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; -import androidx.viewpager2.widget.ViewPager2; -import me.grishka.appkit.fragments.BaseRecyclerFragment; -import me.grishka.appkit.fragments.ToolbarFragment; -import me.grishka.appkit.utils.V; - -public class NotificationsFragment extends MastodonToolbarFragment implements ScrollableToTop{ - - private TabLayout tabLayout; - private ViewPager2 pager; - private FrameLayout[] tabViews; - private TabLayoutMediator tabLayoutMediator; - - private NotificationsListFragment allNotificationsFragment, mentionsFragment; - - private String accountID; - - @Override - public void onCreate(Bundle savedInstanceState){ - super.onCreate(savedInstanceState); - if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N) - setRetainInstance(true); - - accountID=getArguments().getString("account"); - } - - @Override - public void onAttach(Activity activity){ - super.onAttach(activity); - setTitle(R.string.notifications); - } - - @Override - public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ - LinearLayout view=(LinearLayout) inflater.inflate(R.layout.fragment_notifications, container, false); - - tabLayout=view.findViewById(R.id.tabbar); - pager=view.findViewById(R.id.pager); - - tabViews=new FrameLayout[2]; - for(int i=0;i R.id.notifications_all; - case 1 -> R.id.notifications_mentions; - default -> throw new IllegalStateException("Unexpected value: "+i); - }); - tabView.setVisibility(View.GONE); - view.addView(tabView); // needed so the fragment manager will have somewhere to restore the tab fragment - tabViews[i]=tabView; - } - - tabLayout.setTabTextSize(V.dp(16)); - tabLayout.setTabTextColors(UiUtils.getThemeColor(getActivity(), R.attr.colorTabInactive), UiUtils.getThemeColor(getActivity(), android.R.attr.textColorPrimary)); - tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { - @Override - public void onTabSelected(TabLayout.Tab tab) {} - - @Override - public void onTabUnselected(TabLayout.Tab tab) {} - - @Override - public void onTabReselected(TabLayout.Tab tab) { - scrollToTop(); - } - }); - - pager.setOffscreenPageLimit(4); - pager.setAdapter(new DiscoverPagerAdapter()); - pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback(){ - @Override - public void onPageSelected(int position){ - if(position==0) - return; - Fragment _page=getFragmentForPage(position); - if(_page instanceof BaseRecyclerFragment page){ - if(!page.loaded && !page.isDataLoading()) - page.loadData(); - } - } - }); - - if(allNotificationsFragment==null){ - Bundle args=new Bundle(); - args.putString("account", accountID); - args.putBoolean("__is_tab", true); - - allNotificationsFragment=new NotificationsListFragment(); - allNotificationsFragment.setArguments(args); - - args=new Bundle(args); - args.putBoolean("onlyMentions", true); - mentionsFragment=new NotificationsListFragment(); - mentionsFragment.setArguments(args); - - getChildFragmentManager().beginTransaction() - .add(R.id.notifications_all, allNotificationsFragment) - .add(R.id.notifications_mentions, mentionsFragment) - .commit(); - } - - tabLayoutMediator=new TabLayoutMediator(tabLayout, pager, new TabLayoutMediator.TabConfigurationStrategy(){ - @Override - public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){ - tab.setText(switch(position){ - case 0 -> R.string.all_notifications; - case 1 -> R.string.mentions; - default -> throw new IllegalStateException("Unexpected value: "+position); - }); - tab.view.textView.setAllCaps(true); - } - }); - tabLayoutMediator.attach(); - - return view; - } - - @Override - public void scrollToTop(){ - getFragmentForPage(pager.getCurrentItem()).scrollToTop(); - } - - public void loadData(){ - if(allNotificationsFragment!=null && !allNotificationsFragment.loaded && !allNotificationsFragment.dataLoading) - allNotificationsFragment.loadData(); - } - - @Override - protected void updateToolbar(){ - super.updateToolbar(); - getToolbar().setOutlineProvider(null); - getToolbar().setOnClickListener(v->scrollToTop()); - } - - private NotificationsListFragment getFragmentForPage(int page){ - return switch(page){ - case 0 -> allNotificationsFragment; - case 1 -> mentionsFragment; - default -> throw new IllegalStateException("Unexpected value: "+page); - }; - } - - private class DiscoverPagerAdapter extends RecyclerView.Adapter{ - @NonNull - @Override - public SimpleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ - FrameLayout view=tabViews[viewType]; - ((ViewGroup)view.getParent()).removeView(view); - view.setVisibility(View.VISIBLE); - view.setLayoutParams(new RecyclerView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - return new SimpleViewHolder(view); - } - - @Override - public void onBindViewHolder(@NonNull SimpleViewHolder holder, int position){} - - @Override - public int getItemCount(){ - return 2; - } - - @Override - public int getItemViewType(int position){ - return position; - } - } -} 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 a88f05b7..125f5a2a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java @@ -1,7 +1,15 @@ package org.joinmastodon.android.fragments; import android.app.Activity; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; import android.os.Bundle; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import com.squareup.otto.Subscribe; @@ -15,33 +23,44 @@ import org.joinmastodon.android.events.RemoveAccountPostsEvent; import org.joinmastodon.android.model.Notification; import org.joinmastodon.android.model.PaginatedResponse; import org.joinmastodon.android.model.Status; -import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem; -import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem; +import org.joinmastodon.android.ui.OutlineProviders; +import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem; import org.joinmastodon.android.ui.displayitems.StatusDisplayItem; import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration; +import org.joinmastodon.android.ui.utils.UiUtils; +import org.joinmastodon.android.ui.views.NestedRecyclerScrollView; +import org.joinmastodon.android.utils.ObjectIdComparator; import org.parceler.Parcels; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; -import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; +import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import me.grishka.appkit.Nav; import me.grishka.appkit.api.SimpleCallback; -import me.grishka.appkit.utils.V; public class NotificationsListFragment extends BaseStatusListFragment{ - private boolean onlyMentions; + private boolean onlyMentions=true; private String maxID; + private View tabBar; + private View mentionsTab, allTab; + private View endMark; + private String unreadMarker, realUnreadMarker; + private MenuItem markAllReadItem; @Override public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); + setLayout(R.layout.fragment_notifications); E.register(this); + if(savedInstanceState!=null){ + onlyMentions=savedInstanceState.getBoolean("onlyMentions", true); + } + setHasOptionsMenu(true); } @Override @@ -53,28 +72,29 @@ public class NotificationsListFragment extends BaseStatusListFragment buildDisplayItems(Notification n){ - 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); - }; - HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, null, extraText) : null; + 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; + } + } if(n.status!=null){ - ArrayList items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, titleItem!=null, titleItem==null); + 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, flags); if(titleItem!=null) items.add(0, titleItem); return items; }else if(titleItem!=null){ - AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, n.account); - return Arrays.asList(titleItem, card); + return Collections.singletonList(titleItem); }else{ return Collections.emptyList(); } @@ -90,6 +110,7 @@ public class NotificationsListFragment extends BaseStatusListFragment0 ? maxID : null, count, onlyMentions, refreshing, new SimpleCallback<>(this){ @@ -97,39 +118,23 @@ public class NotificationsListFragment extends BaseStatusListFragment> result){ if(getActivity()==null) return; - if(refreshing) - relationships.clear(); onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty()); - Set needRelationships=result.items.stream() - .filter(ntf->ntf.status==null && !relationships.containsKey(ntf.account.id)) - .map(ntf->ntf.account.id) - .collect(Collectors.toSet()); - loadRelationships(needRelationships); maxID=result.maxID; - - if(offset==0 && !result.items.isEmpty()){ - new SaveMarkers(null, result.items.get(0).id).exec(accountID); - } + endMark.setVisibility(result.items.isEmpty() ? View.VISIBLE : View.GONE); } }); } @Override - protected void onRelationshipsLoaded(){ - if(getActivity()==null) - return; - for(int i=0;ilist); + scroller.setTakePriorityOverChildViews(true); + + list.addItemDecoration(new RecyclerView.ItemDecoration(){ + private Paint paint=new Paint(); + private Rect tmpRect=new Rect(); + + { + paint.setColor(UiUtils.getThemeColor(getActivity(), R.attr.colorM3SurfaceVariant)); + } + + @Override + public void onDraw(@NonNull Canvas c, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ + if(TextUtils.isEmpty(unreadMarker)) + return; + for(int i=0;i holder){ + String itemID=holder.getItemID(); + if(ObjectIdComparator.INSTANCE.compare(itemID, unreadMarker)>0){ + parent.getDecoratedBoundsWithMargins(child, tmpRect); + c.drawRect(tmpRect, paint); + } + } + } + } + }); + } + + @Override + protected List getViewsForElevationEffect(){ + ArrayList views=new ArrayList<>(super.getViewsForElevationEffect()); + views.add(tabBar); + return views; + } + + @Override + public void onSaveInstanceState(Bundle outState){ + super.onSaveInstanceState(outState); + outState.putBoolean("onlyMentions", onlyMentions); } private Notification getNotificationByID(String id){ @@ -211,4 +270,85 @@ public class NotificationsListFragment extends BaseStatusListFragment=adapter.getItemCount()); + } + + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ + inflater.inflate(R.menu.notifications, menu); + markAllReadItem=menu.findItem(R.id.mark_all_read); + updateMarkAllReadButton(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item){ + if(item.getItemId()==R.id.mark_all_read){ + markAsRead(); + resetUnreadBackground(); + } + return true; + } + + private void markAsRead(){ + String id=data.get(0).id; + new SaveMarkers(null, id).exec(accountID); + AccountSessionManager.get(accountID).setNotificationsMarker(id, true); + realUnreadMarker=id; + updateMarkAllReadButton(); + } + + private void resetUnreadBackground(){ + unreadMarker=realUnreadMarker; + list.invalidate(); + } + + @Override + public void onRefresh(){ + super.onRefresh(); + resetUnreadBackground(); + AccountSessionManager.get(accountID).reloadNotificationsMarker(m->{ + unreadMarker=realUnreadMarker=m; + }); + } + + private void updateMarkAllReadButton(){ + markAllReadItem.setEnabled(!data.isEmpty() && !realUnreadMarker.equals(data.get(0).id)); + } + + @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){ + markAsRead(); + break; + } + } + } } 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 00000000..aabe6df5 --- /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/AccountCardStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AccountCardStatusDisplayItem.java deleted file mode 100644 index 9fb35dde..00000000 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/AccountCardStatusDisplayItem.java +++ /dev/null @@ -1,166 +0,0 @@ -package org.joinmastodon.android.ui.displayitems; - -import android.app.Activity; -import android.content.Context; -import android.graphics.drawable.Animatable; -import android.graphics.drawable.Drawable; -import android.text.SpannableStringBuilder; -import android.text.TextUtils; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.ProgressBar; -import android.widget.TextView; - -import org.joinmastodon.android.R; -import org.joinmastodon.android.fragments.BaseStatusListFragment; -import org.joinmastodon.android.model.Account; -import org.joinmastodon.android.model.Relationship; -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.joinmastodon.android.ui.views.ProgressBarButton; - -import java.util.Collections; - -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 AccountCardStatusDisplayItem extends StatusDisplayItem{ - private final Account account; - public ImageLoaderRequest avaRequest, coverRequest; - public CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); - public CharSequence parsedName, parsedBio; - - public AccountCardStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Account account){ - super(parentID, parentFragment); - this.account=account; - if(!TextUtils.isEmpty(account.avatar)) - avaRequest=new UrlImageLoaderRequest(account.avatar, V.dp(50), V.dp(50)); - if(!TextUtils.isEmpty(account.header)) - coverRequest=new UrlImageLoaderRequest(account.header, 1000, 1000); - parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), parentFragment.getAccountID()); - if(account.emojis.isEmpty()){ - parsedName=account.displayName; - }else{ - parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis); - emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio)); - } - } - - @Override - public Type getType(){ - return Type.ACCOUNT_CARD; - } - - @Override - public int getImageCount(){ - return 2+emojiHelper.getImageCount(); - } - - @Override - public ImageLoaderRequest getImageRequest(int index){ - return switch(index){ - case 0 -> avaRequest; - case 1 -> coverRequest; - default -> emojiHelper.getImageRequest(index-2); - }; - } - - public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ - private final ImageView cover, avatar; - private final TextView name, username, bio, followersCount, followingCount, postsCount, followersLabel, followingLabel, postsLabel; - private final ProgressBarButton actionButton; - private final ProgressBar actionProgress; - private final View actionWrap; - - private Relationship relationship; - - public Holder(Context context, ViewGroup parent){ - super(context, R.layout.display_item_account_card, parent); - - cover=findViewById(R.id.cover); - avatar=findViewById(R.id.avatar); - name=findViewById(R.id.name); - username=findViewById(R.id.username); - bio=findViewById(R.id.bio); - followersCount=findViewById(R.id.followers_count); - followersLabel=findViewById(R.id.followers_label); - followingCount=findViewById(R.id.following_count); - followingLabel=findViewById(R.id.following_label); - postsCount=findViewById(R.id.posts_count); - postsLabel=findViewById(R.id.posts_label); - actionButton=findViewById(R.id.action_btn); - actionProgress=findViewById(R.id.action_progress); - actionWrap=findViewById(R.id.action_btn_wrap); - - View card=findViewById(R.id.card); - card.setOutlineProvider(OutlineProviders.roundedRect(6)); - card.setClipToOutline(true); - avatar.setOutlineProvider(OutlineProviders.roundedRect(12)); - avatar.setClipToOutline(true); - cover.setOutlineProvider(OutlineProviders.roundedRect(3)); - cover.setClipToOutline(true); - actionButton.setOnClickListener(this::onActionButtonClick); - } - - @Override - public void onBind(AccountCardStatusDisplayItem item){ - name.setText(item.parsedName); - username.setText('@'+item.account.acct); - bio.setText(item.parsedBio); - followersCount.setText(UiUtils.abbreviateNumber(item.account.followersCount)); - followingCount.setText(UiUtils.abbreviateNumber(item.account.followingCount)); - postsCount.setText(UiUtils.abbreviateNumber(item.account.statusesCount)); - followersLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount))); - followingLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount))); - postsLabel.setText(item.parentFragment.getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount))); - relationship=item.parentFragment.getRelationship(item.account.id); - if(relationship==null){ - actionWrap.setVisibility(View.GONE); - }else{ - actionWrap.setVisibility(View.VISIBLE); - UiUtils.setRelationshipToActionButton(relationship, actionButton); - } - } - - - private void onActionButtonClick(View v){ - itemView.setHasTransientState(true); - UiUtils.performAccountAction((Activity) v.getContext(), item.account, item.parentFragment.getAccountID(), relationship, actionButton, this::setActionProgressVisible, rel->{ - itemView.setHasTransientState(false); - item.parentFragment.putRelationship(item.account.id, rel); - rebind(); - }); - } - - private void setActionProgressVisible(boolean visible){ - actionButton.setTextVisible(!visible); - actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE); - actionButton.setClickable(!visible); - } - - @Override - public void setImage(int index, Drawable image){ - if(index==0){ - avatar.setImageDrawable(image); - }else if(index==1){ - cover.setImageDrawable(image); - }else{ - item.emojiHelper.setImageDrawable(index-2, image); - name.invalidate(); - bio.invalidate(); - } - if(image instanceof Animatable && !((Animatable) image).isRunning()) - ((Animatable) image).start(); - } - - @Override - public void clearImage(int index){ - setImage(index, null); - } - } -} 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 00000000..82d2e368 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/NotificationHeaderStatusDisplayItem.java @@ -0,0 +1,152 @@ +package org.joinmastodon.android.ui.displayitems; + +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.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; + default -> throw new IllegalStateException("Unexpected value: "+notification.type); + }).split("%s", 2); + 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(' '); + text.append(parts[0]); + }else if(!TextUtils.isEmpty(parts[1])){ + text.append(parts[1]); + } + 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; + + 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); + avatar.setOnClickListener(this::onAvaClick); + } + + @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); + } + + @Override + public void onBind(NotificationHeaderStatusDisplayItem item){ + text.setText(item.text); + avatar.setVisibility(item.notification.type==Notification.Type.POLL ? View.GONE : View.VISIBLE); + // TODO use real icons + icon.setImageResource(switch(item.notification.type){ + case FAVORITE -> R.drawable.ic_star_fill1_24px; + case REBLOG -> R.drawable.ic_repeat_fill1_24px; + case FOLLOW, FOLLOW_REQUEST -> R.drawable.ic_person_add_fill1_24px; + case POLL -> R.drawable.ic_insert_chart_fill1_24px; + 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 -> throw new IllegalStateException("Unexpected value: "+item.notification.type); + }))); + } + + private void onAvaClick(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 b10beaaa..51fd1f0e 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 @@ -13,6 +13,7 @@ import org.joinmastodon.android.model.Poll; 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 java.util.Locale; @@ -65,7 +66,7 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{ public static class Holder extends StatusDisplayItem.Holder implements ImageLoaderViewHolder{ private final TextView text, percent; private final View check, button; - private final Drawable progressBg; + private final Drawable progressBg, progressBgInset; public Holder(Activity activity, ViewGroup parent){ super(activity, R.layout.display_item_poll_option, parent); @@ -74,6 +75,7 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{ check=findViewById(R.id.checkbox); button=findViewById(R.id.button); progressBg=activity.getResources().getDrawable(R.drawable.bg_poll_option_voted, activity.getTheme()).mutate(); + progressBgInset=activity.getResources().getDrawable(R.drawable.bg_poll_option_voted_inset, activity.getTheme()).mutate(); itemView.setOnClickListener(this::onButtonClick); button.setOutlineProvider(OutlineProviders.roundedRect(20)); button.setClipToOutline(true); @@ -85,13 +87,21 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{ percent.setVisibility(item.showResults ? View.VISIBLE : View.GONE); itemView.setClickable(!item.showResults); if(item.showResults){ - progressBg.setLevel(Math.round(10000f*item.votesFraction)); - button.setBackground(progressBg); + Drawable bg=item.inset ? progressBgInset : progressBg; + bg.setLevel(Math.round(10000f*item.votesFraction)); + button.setBackground(bg); itemView.setSelected(item.isMostVoted); 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)); - button.setBackgroundResource(R.drawable.bg_poll_option_clickable); + button.setBackgroundResource(item.inset ? R.drawable.bg_poll_option_clickable_inset : R.drawable.bg_poll_option_clickable); + } + if(item.inset){ + text.setTextColor(itemView.getContext().getColorStateList(R.color.poll_option_text_inset)); + percent.setTextColor(itemView.getContext().getColorStateList(R.color.poll_option_text_inset)); + }else{ + text.setTextColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3Primary)); + percent.setTextColor(UiUtils.getThemeColor(itemView.getContext(), R.attr.colorM3OnSecondaryContainer)); } } 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 55086715..782acc76 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 @@ -37,6 +37,7 @@ public abstract class StatusDisplayItem{ public static final int FLAG_NO_FOOTER=1 << 1; public static final int FLAG_CHECKABLE=1 << 2; public static final int FLAG_MEDIA_FORCE_HIDDEN=1 << 3; + public static final int FLAG_NO_HEADER=1 << 4; public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){ this.parentID=parentID; @@ -64,7 +65,6 @@ public abstract class StatusDisplayItem{ case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent); case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent); case FOOTER -> new FooterStatusDisplayItem.Holder(activity, parent); - case ACCOUNT_CARD -> new AccountCardStatusDisplayItem.Holder(activity, parent); case ACCOUNT -> new AccountStatusDisplayItem.Holder(activity, parent); case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent); case GAP -> new GapStatusDisplayItem.Holder(activity, parent); @@ -72,6 +72,7 @@ public abstract class StatusDisplayItem{ case MEDIA_GRID -> new MediaGridStatusDisplayItem.Holder(activity, parent); case SPOILER -> new SpoilerStatusDisplayItem.Holder(activity, parent); case SECTION_HEADER -> new SectionHeaderStatusDisplayItem.Holder(activity, parent); + case NOTIFICATION_HEADER -> new NotificationHeaderStatusDisplayItem.Holder(activity, parent); }; } @@ -88,17 +89,19 @@ public abstract class StatusDisplayItem{ String parentID=parentObject.getID(); ArrayList items=new ArrayList<>(); Status statusForContent=status.getContentStatus(); - if(status.reblog!=null){ - items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_repeat_20px)); - }else if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId)){ - Account account=Objects.requireNonNull(knownAccounts.get(status.inReplyToAccountId)); - items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_reply_20px)); + HeaderStatusDisplayItem header=null; + if((flags & FLAG_NO_HEADER)==0){ + if(status.reblog!=null){ + items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_repeat_20px)); + }else if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId)){ + Account account=Objects.requireNonNull(knownAccounts.get(status.inReplyToAccountId)); + items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_reply_20px)); + } + 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)); } - HeaderStatusDisplayItem header; - 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)); ArrayList contentItems; if(!TextUtils.isEmpty(statusForContent.spoilerText)){ @@ -109,10 +112,13 @@ public abstract class StatusDisplayItem{ contentItems=items; } - if(!TextUtils.isEmpty(statusForContent.content)) - contentItems.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent)); - else + if(!TextUtils.isEmpty(statusForContent.content)){ + TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent); + text.reduceTopPadding=header==null; + contentItems.add(text); + }else if(header!=null){ header.needBottomPadding=true; + } List imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList()); if(!imageAttachments.isEmpty()){ @@ -171,7 +177,6 @@ public abstract class StatusDisplayItem{ POLL_FOOTER, CARD, FOOTER, - ACCOUNT_CARD, ACCOUNT, HASHTAG, GAP, @@ -179,7 +184,8 @@ public abstract class StatusDisplayItem{ MEDIA_GRID, SPOILER, SECTION_HEADER, - HEADER_CHECKABLE + HEADER_CHECKABLE, + NOTIFICATION_HEADER } public static abstract class Holder extends BindableViewHolder implements UsableRecyclerView.DisableableClickable{ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java index 79414bce..6a413bbc 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/displayitems/TextStatusDisplayItem.java @@ -9,16 +9,19 @@ import org.joinmastodon.android.R; import org.joinmastodon.android.fragments.BaseStatusListFragment; import org.joinmastodon.android.model.Status; import org.joinmastodon.android.ui.utils.CustomEmojiHelper; +import org.joinmastodon.android.ui.utils.UiUtils; import org.joinmastodon.android.ui.views.LinkedTextView; import me.grishka.appkit.imageloader.ImageLoaderViewHolder; import me.grishka.appkit.imageloader.MovieDrawable; import me.grishka.appkit.imageloader.requests.ImageLoaderRequest; +import me.grishka.appkit.utils.V; public class TextStatusDisplayItem extends StatusDisplayItem{ private CharSequence text; private CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); public boolean textSelectable; + public boolean reduceTopPadding; public final Status status; public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status){ @@ -57,6 +60,8 @@ public class TextStatusDisplayItem extends StatusDisplayItem{ text.setTextIsSelectable(item.textSelectable); text.setInvalidateOnEveryFrame(false); itemView.setClickable(false); + text.setPadding(text.getPaddingLeft(), item.reduceTopPadding ? V.dp(8) : V.dp(16), text.getPaddingRight(), text.getPaddingBottom()); + text.setTextColor(UiUtils.getThemeColor(text.getContext(), item.inset ? R.attr.colorM3OnSurfaceVariant : R.attr.colorM3OnSurface)); } @Override diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java index 12581b09..d94d8f90 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/InsetStatusItemDecoration.java @@ -28,8 +28,8 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{ public InsetStatusItemDecoration(BaseStatusListFragment listFragment){ this.listFragment=listFragment; - bgColor=UiUtils.getThemeColor(listFragment.getActivity(), android.R.attr.colorBackground); - borderColor=UiUtils.getThemeColor(listFragment.getActivity(), R.attr.colorPollVoted); + bgColor=UiUtils.getThemeColor(listFragment.getActivity(), R.attr.colorM3SurfaceVariant); + borderColor=UiUtils.getThemeColor(listFragment.getActivity(), R.attr.colorM3OutlineVariant); } @Override @@ -64,9 +64,8 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{ private void drawInsetBackground(RecyclerView list, Canvas c){ paint.setStyle(Paint.Style.FILL); paint.setColor(bgColor); - rect.left=V.dp(12); - rect.right=list.getWidth()-V.dp(12); - rect.inset(V.dp(4), V.dp(4)); + rect.left=V.dp(16); + rect.right=list.getWidth()-V.dp(16); c.drawRoundRect(rect, V.dp(4), V.dp(4), paint); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(V.dp(1)); @@ -85,20 +84,15 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{ if(inset){ boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset; boolean bottomSiblingInset=pos scrollableChildSupplier; + private boolean takePriorityOverChildViews; public NestedRecyclerScrollView(Context context){ super(context); @@ -25,32 +26,43 @@ public class NestedRecyclerScrollView extends CustomScrollView{ } @Override - public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { - if(target instanceof RecyclerView rv && ((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 && target instanceof RecyclerView rv && isScrolledToTop(rv)) || (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) { - if (target instanceof RecyclerView rv && ((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 && target instanceof RecyclerView rv && isScrolledToTop(rv)) || (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(RecyclerView rv){ + final LinearLayoutManager lm=(LinearLayoutManager) rv.getLayoutManager(); + return lm.findFirstVisibleItemPosition()==0 + && lm.findViewByPosition(0).getTop()==rv.getPaddingTop(); } public void setScrollableChildSupplier(Supplier scrollableChildSupplier){ @@ -59,12 +71,20 @@ public class NestedRecyclerScrollView extends CustomScrollView{ @Override protected boolean onScrollingHitEdge(float velocity){ - if(velocity>0){ + if(velocity>0 || takePriorityOverChildViews){ RecyclerView view=scrollableChildSupplier.get(); if(view!=null){ - return view.fling(0, (int)velocity); + return view.fling(0, (int) velocity); } } 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 00000000..975f6a27 --- /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/res/color/poll_option_progress_inset.xml b/mastodon/src/main/res/color/poll_option_progress_inset.xml new file mode 100644 index 00000000..3fd78b91 --- /dev/null +++ b/mastodon/src/main/res/color/poll_option_progress_inset.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/color/poll_option_text_inset.xml b/mastodon/src/main/res/color/poll_option_text_inset.xml new file mode 100644 index 00000000..ef1f151e --- /dev/null +++ b/mastodon/src/main/res/color/poll_option_text_inset.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file 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 00000000..4f7f4bd8 --- /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_poll_option_clickable_inset.xml b/mastodon/src/main/res/drawable/bg_poll_option_clickable_inset.xml new file mode 100644 index 00000000..29dd2b1f --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_poll_option_clickable_inset.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/drawable/bg_poll_option_voted_inset.xml b/mastodon/src/main/res/drawable/bg_poll_option_voted_inset.xml new file mode 100644 index 00000000..1bc8c099 --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_poll_option_voted_inset.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ 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 00000000..f6b62e43 --- /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 00000000..ad45941f --- /dev/null +++ b/mastodon/src/main/res/drawable/bg_tabbar_badge.xml @@ -0,0 +1,5 @@ + + + + + \ 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 00000000..0e355227 --- /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 00000000..c2052ae4 --- /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 00000000..e792d799 --- /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_person_add_fill1_24px.xml b/mastodon/src/main/res/drawable/ic_person_add_fill1_24px.xml new file mode 100644 index 00000000..0939052e --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_person_add_fill1_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_repeat_fill1_24px.xml b/mastodon/src/main/res/drawable/ic_repeat_fill1_24px.xml new file mode 100644 index 00000000..7bf54f96 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_repeat_fill1_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/drawable/ic_star_fill1_24px.xml b/mastodon/src/main/res/drawable/ic_star_fill1_24px.xml new file mode 100644 index 00000000..a3fce1e2 --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_star_fill1_24px.xml @@ -0,0 +1,9 @@ + + + diff --git a/mastodon/src/main/res/layout/display_item_account_card.xml b/mastodon/src/main/res/layout/display_item_account_card.xml deleted file mode 100644 index 5d98ba6e..00000000 --- a/mastodon/src/main/res/layout/display_item_account_card.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ 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 00000000..fa5f8a4f --- /dev/null +++ b/mastodon/src/main/res/layout/display_item_notification_header.xml @@ -0,0 +1,34 @@ + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/fragment_notifications.xml b/mastodon/src/main/res/layout/fragment_notifications.xml index 456ecadf..7a820afa 100644 --- a/mastodon/src/main/res/layout/fragment_notifications.xml +++ b/mastodon/src/main/res/layout/fragment_notifications.xml @@ -1,25 +1,84 @@ - + android:id="@+id/appkit_loader_root" + xmlns:android="http://schemas.android.com/apk/res/android" + android:background="?colorM3Surface"> - + android:layout_height="match_parent" + android:fillViewport="true"> - + - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/layout/load_more_with_end_mark.xml b/mastodon/src/main/res/layout/load_more_with_end_mark.xml new file mode 100644 index 00000000..37f3eb24 --- /dev/null +++ b/mastodon/src/main/res/layout/load_more_with_end_mark.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + \ 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 fa940847..be22305c 100644 --- a/mastodon/src/main/res/layout/tab_bar.xml +++ b/mastodon/src/main/res/layout/tab_bar.xml @@ -73,6 +73,22 @@ android:importantForAccessibility="no" android:tint="?colorM3OnSurfaceVariant" android:src="@drawable/ic_notifications_24px"/> + + diff --git a/mastodon/src/main/res/menu/notifications.xml b/mastodon/src/main/res/menu/notifications.xml new file mode 100644 index 00000000..33b2b449 --- /dev/null +++ b/mastodon/src/main/res/menu/notifications.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/values/attrs.xml b/mastodon/src/main/res/values/attrs.xml index 1531561c..d6a20167 100644 --- a/mastodon/src/main/res/values/attrs.xml +++ b/mastodon/src/main/res/values/attrs.xml @@ -45,6 +45,10 @@ + + + + diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index 5380da9d..6a3041ed 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 @@ -139,7 +139,7 @@ Hashtags News For you - All + Everything Mentions %d person is talking @@ -502,4 +502,5 @@ You’ve already blocked this user, so there’s nothing else you need to do while we review your report. You’ve already blocked this user, so there’s nothing else you need to do.\n\nThanks for helping keep Mastodon a safe place for everyone! Blocked %s + Mark all as read \ No newline at end of file diff --git a/mastodon/src/main/res/values/styles.xml b/mastodon/src/main/res/values/styles.xml index 6a2b8516..28e9a816 100644 --- a/mastodon/src/main/res/values/styles.xml +++ b/mastodon/src/main/res/values/styles.xml @@ -68,6 +68,10 @@ #410E0B @color/m3_sys_dark_primary #FFF + #8b5000 + #ab332a + #4746e3 + #006d42 ?colorM3Background ?colorM3Background @@ -146,6 +150,10 @@ #F9DEDC @color/m3_sys_light_primary #000 + #ffb871 + #ffb4aa + #c1c1ff + #77daa1 ?colorM3Background ?colorM3Background @@ -357,6 +365,30 @@ ?colorM3OnSurface + + + + + +