Notifications M3 redesign (+ read marker support)
This commit is contained in:
parent
92335d8678
commit
5c480b37b3
|
@ -42,6 +42,8 @@ public class CacheController{
|
||||||
private final String accountID;
|
private final String accountID;
|
||||||
private DatabaseHelper db;
|
private DatabaseHelper db;
|
||||||
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
private final Runnable databaseCloseRunnable=this::closeDatabase;
|
||||||
|
private boolean loadingNotifications;
|
||||||
|
private final ArrayList<Callback<PaginatedResponse<List<Notification>>>> pendingNotificationsCallbacks=new ArrayList<>();
|
||||||
|
|
||||||
private static final int POST_FLAG_GAP_AFTER=1;
|
private static final int POST_FLAG_GAP_AFTER=1;
|
||||||
|
|
||||||
|
@ -131,6 +133,12 @@ public class CacheController{
|
||||||
cancelDelayedClose();
|
cancelDelayedClose();
|
||||||
databaseThread.postRunnable(()->{
|
databaseThread.postRunnable(()->{
|
||||||
try{
|
try{
|
||||||
|
if(!onlyMentions && loadingNotifications){
|
||||||
|
synchronized(pendingNotificationsCallbacks){
|
||||||
|
pendingNotificationsCallbacks.add(callback);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
|
List<Filter> filters=AccountSessionManager.getInstance().getAccount(accountID).wordFilters.stream().filter(f->f.context.contains(Filter.FilterContext.NOTIFICATIONS)).collect(Collectors.toList());
|
||||||
if(!forceReload){
|
if(!forceReload){
|
||||||
SQLiteDatabase db=getOrOpenDatabase();
|
SQLiteDatabase db=getOrOpenDatabase();
|
||||||
|
@ -160,11 +168,13 @@ public class CacheController{
|
||||||
Log.w(TAG, "getNotifications: corrupted notification object in database", x);
|
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))
|
new GetNotifications(maxID, count, onlyMentions ? EnumSet.of(Notification.Type.MENTION): EnumSet.allOf(Notification.Type.class))
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<Notification> result){
|
public void onSuccess(List<Notification> result){
|
||||||
callback.onSuccess(new PaginatedResponse<>(result.stream().filter(ntf->{
|
PaginatedResponse<List<Notification>> res=new PaginatedResponse<>(result.stream().filter(ntf->{
|
||||||
if(ntf.status!=null){
|
if(ntf.status!=null){
|
||||||
for(Filter filter:filters){
|
for(Filter filter:filters){
|
||||||
if(filter.matches(ntf.status)){
|
if(filter.matches(ntf.status)){
|
||||||
|
@ -173,13 +183,32 @@ public class CacheController{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
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);
|
putNotifications(result, onlyMentions, maxID==null);
|
||||||
|
if(!onlyMentions){
|
||||||
|
loadingNotifications=false;
|
||||||
|
synchronized(pendingNotificationsCallbacks){
|
||||||
|
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
|
||||||
|
cb.onSuccess(res);
|
||||||
|
}
|
||||||
|
pendingNotificationsCallbacks.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onError(ErrorResponse error){
|
public void onError(ErrorResponse error){
|
||||||
callback.onError(error);
|
callback.onError(error);
|
||||||
|
if(!onlyMentions){
|
||||||
|
loadingNotifications=false;
|
||||||
|
synchronized(pendingNotificationsCallbacks){
|
||||||
|
for(Callback<PaginatedResponse<List<Notification>>> cb:pendingNotificationsCallbacks){
|
||||||
|
cb.onError(error);
|
||||||
|
}
|
||||||
|
pendingNotificationsCallbacks.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.exec(accountID);
|
.exec(accountID);
|
||||||
|
|
|
@ -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<TimelineMarkers>{
|
||||||
|
public GetMarkers(){
|
||||||
|
super(HttpMethod.GET, "/markers", TimelineMarkers.class);
|
||||||
|
addQueryParameter("timeline[]", "home");
|
||||||
|
addQueryParameter("timeline[]", "notifications");
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,11 +2,11 @@ package org.joinmastodon.android.api.requests.markers;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
import org.joinmastodon.android.api.gson.JsonObjectBuilder;
|
import org.joinmastodon.android.api.gson.JsonObjectBuilder;
|
||||||
import org.joinmastodon.android.model.Marker;
|
import org.joinmastodon.android.model.TimelineMarkers;
|
||||||
|
|
||||||
public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
|
public class SaveMarkers extends MastodonAPIRequest<TimelineMarkers>{
|
||||||
public SaveMarkers(String lastSeenHomePostID, String lastSeenNotificationID){
|
public SaveMarkers(String lastSeenHomePostID, String lastSeenNotificationID){
|
||||||
super(HttpMethod.POST, "/markers", Response.class);
|
super(HttpMethod.POST, "/markers", TimelineMarkers.class);
|
||||||
JsonObjectBuilder builder=new JsonObjectBuilder();
|
JsonObjectBuilder builder=new JsonObjectBuilder();
|
||||||
if(lastSeenHomePostID!=null)
|
if(lastSeenHomePostID!=null)
|
||||||
builder.add("home", new JsonObjectBuilder().add("last_read_id", lastSeenHomePostID));
|
builder.add("home", new JsonObjectBuilder().add("last_read_id", lastSeenHomePostID));
|
||||||
|
@ -14,8 +14,4 @@ public class SaveMarkers extends MastodonAPIRequest<SaveMarkers.Response>{
|
||||||
builder.add("notifications", new JsonObjectBuilder().add("last_read_id", lastSeenNotificationID));
|
builder.add("notifications", new JsonObjectBuilder().add("last_read_id", lastSeenNotificationID));
|
||||||
setRequestBody(builder.build());
|
setRequestBody(builder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Response{
|
|
||||||
public Marker home, notifications;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,28 @@
|
||||||
package org.joinmastodon.android.api.session;
|
package org.joinmastodon.android.api.session;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
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.CacheController;
|
||||||
import org.joinmastodon.android.api.MastodonAPIController;
|
import org.joinmastodon.android.api.MastodonAPIController;
|
||||||
import org.joinmastodon.android.api.PushSubscriptionManager;
|
import org.joinmastodon.android.api.PushSubscriptionManager;
|
||||||
import org.joinmastodon.android.api.StatusInteractionController;
|
import org.joinmastodon.android.api.StatusInteractionController;
|
||||||
import org.joinmastodon.android.api.requests.accounts.GetPreferences;
|
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.Account;
|
||||||
import org.joinmastodon.android.model.Application;
|
import org.joinmastodon.android.model.Application;
|
||||||
import org.joinmastodon.android.model.Filter;
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.Preferences;
|
import org.joinmastodon.android.model.Preferences;
|
||||||
import org.joinmastodon.android.model.PushSubscription;
|
import org.joinmastodon.android.model.PushSubscription;
|
||||||
|
import org.joinmastodon.android.model.TimelineMarkers;
|
||||||
import org.joinmastodon.android.model.Token;
|
import org.joinmastodon.android.model.Token;
|
||||||
|
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -44,6 +54,7 @@ public class AccountSession{
|
||||||
private transient StatusInteractionController statusInteractionController;
|
private transient StatusInteractionController statusInteractionController;
|
||||||
private transient CacheController cacheController;
|
private transient CacheController cacheController;
|
||||||
private transient PushSubscriptionManager pushSubscriptionManager;
|
private transient PushSubscriptionManager pushSubscriptionManager;
|
||||||
|
private transient SharedPreferences prefs;
|
||||||
|
|
||||||
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
AccountSession(Token token, Account self, Application app, String domain, boolean activated, AccountActivationInfo activationInfo){
|
||||||
this.token=token;
|
this.token=token;
|
||||||
|
@ -106,4 +117,44 @@ public class AccountSession{
|
||||||
})
|
})
|
||||||
.exec(getID());
|
.exec(getID());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public SharedPreferences getLocalPreferences(){
|
||||||
|
if(prefs==null)
|
||||||
|
prefs=MastodonApp.context.getSharedPreferences(getID(), Context.MODE_PRIVATE);
|
||||||
|
return prefs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reloadNotificationsMarker(Consumer<String> 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -142,6 +142,10 @@ public class AccountSessionManager{
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static AccountSession get(String id){
|
||||||
|
return getInstance().getAccount(id);
|
||||||
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
public AccountSession tryGetAccount(String id){
|
public AccountSession tryGetAccount(String id){
|
||||||
return sessions.get(id);
|
return sessions.get(id);
|
||||||
|
@ -174,6 +178,12 @@ public class AccountSessionManager{
|
||||||
AccountSession session=getAccount(id);
|
AccountSession session=getAccount(id);
|
||||||
session.getCacheController().closeDatabase();
|
session.getCacheController().closeDatabase();
|
||||||
MastodonApp.context.deleteDatabase(id+".db");
|
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);
|
sessions.remove(id);
|
||||||
if(lastActiveAccountID.equals(id)){
|
if(lastActiveAccountID.equals(id)){
|
||||||
if(sessions.isEmpty())
|
if(sessions.isEmpty())
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,12 @@
|
||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Fragment;
|
import android.app.Fragment;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.graphics.Outline;
|
import android.graphics.Outline;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
@ -14,14 +16,23 @@ import android.view.WindowInsets;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
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.PushNotificationReceiver;
|
||||||
import org.joinmastodon.android.R;
|
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.AccountSession;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
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.discover.DiscoverFragment;
|
||||||
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
|
import org.joinmastodon.android.fragments.onboarding.OnboardingFollowSuggestionsFragment;
|
||||||
import org.joinmastodon.android.model.Account;
|
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.AccountSwitcherSheet;
|
||||||
import org.joinmastodon.android.ui.OutlineProviders;
|
import org.joinmastodon.android.ui.OutlineProviders;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
@ -29,11 +40,14 @@ import org.joinmastodon.android.ui.views.TabBar;
|
||||||
import org.parceler.Parcels;
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import androidx.annotation.IdRes;
|
import androidx.annotation.IdRes;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import me.grishka.appkit.FragmentStackActivity;
|
import me.grishka.appkit.FragmentStackActivity;
|
||||||
import me.grishka.appkit.Nav;
|
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.AppKitFragment;
|
||||||
import me.grishka.appkit.fragments.LoaderFragment;
|
import me.grishka.appkit.fragments.LoaderFragment;
|
||||||
import me.grishka.appkit.fragments.OnBackPressedListener;
|
import me.grishka.appkit.fragments.OnBackPressedListener;
|
||||||
|
@ -45,7 +59,7 @@ import me.grishka.appkit.views.FragmentRootLinearLayout;
|
||||||
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
|
public class HomeFragment extends AppKitFragment implements OnBackPressedListener{
|
||||||
private FragmentRootLinearLayout content;
|
private FragmentRootLinearLayout content;
|
||||||
private HomeTimelineFragment homeTimelineFragment;
|
private HomeTimelineFragment homeTimelineFragment;
|
||||||
private NotificationsFragment notificationsFragment;
|
private NotificationsListFragment notificationsFragment;
|
||||||
private DiscoverFragment searchFragment;
|
private DiscoverFragment searchFragment;
|
||||||
private ProfileFragment profileFragment;
|
private ProfileFragment profileFragment;
|
||||||
private TabBar tabBar;
|
private TabBar tabBar;
|
||||||
|
@ -53,6 +67,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||||
private ImageView tabBarAvatar;
|
private ImageView tabBarAvatar;
|
||||||
@IdRes
|
@IdRes
|
||||||
private int currentTab=R.id.tab_home;
|
private int currentTab=R.id.tab_home;
|
||||||
|
private TextView notificationsBadge;
|
||||||
|
|
||||||
private String accountID;
|
private String accountID;
|
||||||
|
|
||||||
|
@ -74,7 +89,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||||
args.putBoolean("noAutoLoad", true);
|
args.putBoolean("noAutoLoad", true);
|
||||||
searchFragment=new DiscoverFragment();
|
searchFragment=new DiscoverFragment();
|
||||||
searchFragment.setArguments(args);
|
searchFragment.setArguments(args);
|
||||||
notificationsFragment=new NotificationsFragment();
|
notificationsFragment=new NotificationsListFragment();
|
||||||
notificationsFragment.setArguments(args);
|
notificationsFragment.setArguments(args);
|
||||||
args=new Bundle(args);
|
args=new Bundle(args);
|
||||||
args.putParcelable("profileAccount", Parcels.wrap(AccountSessionManager.getInstance().getAccount(accountID).self));
|
args.putParcelable("profileAccount", Parcels.wrap(AccountSessionManager.getInstance().getAccount(accountID).self));
|
||||||
|
@ -83,6 +98,13 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||||
profileFragment.setArguments(args);
|
profileFragment.setArguments(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
E.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy(){
|
||||||
|
super.onDestroy();
|
||||||
|
E.unregister(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -106,6 +128,9 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||||
Account self=AccountSessionManager.getInstance().getAccount(accountID).self;
|
Account self=AccountSessionManager.getInstance().getAccount(accountID).self;
|
||||||
ViewImageLoader.loadWithoutAnimation(tabBarAvatar, null, new UrlImageLoaderRequest(self.avatar, V.dp(24), V.dp(24)));
|
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){
|
if(savedInstanceState==null){
|
||||||
getChildFragmentManager().beginTransaction()
|
getChildFragmentManager().beginTransaction()
|
||||||
.add(R.id.fragment_wrap, homeTimelineFragment)
|
.add(R.id.fragment_wrap, homeTimelineFragment)
|
||||||
|
@ -138,7 +163,7 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||||
return;
|
return;
|
||||||
homeTimelineFragment=(HomeTimelineFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTimelineFragment");
|
homeTimelineFragment=(HomeTimelineFragment) getChildFragmentManager().getFragment(savedInstanceState, "homeTimelineFragment");
|
||||||
searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment");
|
searchFragment=(DiscoverFragment) getChildFragmentManager().getFragment(savedInstanceState, "searchFragment");
|
||||||
notificationsFragment=(NotificationsFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
|
notificationsFragment=(NotificationsListFragment) getChildFragmentManager().getFragment(savedInstanceState, "notificationsFragment");
|
||||||
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
|
profileFragment=(ProfileFragment) getChildFragmentManager().getFragment(savedInstanceState, "profileFragment");
|
||||||
currentTab=savedInstanceState.getInt("selectedTab");
|
currentTab=savedInstanceState.getInt("selectedTab");
|
||||||
tabBar.selectTab(currentTab);
|
tabBar.selectTab(currentTab);
|
||||||
|
@ -224,9 +249,8 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
|
||||||
lf.loadData();
|
lf.loadData();
|
||||||
}else if(newFragment instanceof DiscoverFragment){
|
}else if(newFragment instanceof DiscoverFragment){
|
||||||
((DiscoverFragment) newFragment).loadData();
|
((DiscoverFragment) newFragment).loadData();
|
||||||
}else if(newFragment instanceof NotificationsFragment){
|
}
|
||||||
((NotificationsFragment) newFragment).loadData();
|
if(newFragment instanceof NotificationsListFragment){
|
||||||
// TODO make an interface?
|
|
||||||
NotificationManager nm=getActivity().getSystemService(NotificationManager.class);
|
NotificationManager nm=getActivity().getSystemService(NotificationManager.class);
|
||||||
nm.cancel(accountID, PushNotificationReceiver.NOTIFICATION_ID);
|
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, "notificationsFragment", notificationsFragment);
|
||||||
getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment);
|
getChildFragmentManager().putFragment(outState, "profileFragment", profileFragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onShown(){
|
||||||
|
super.onShown();
|
||||||
|
reloadNotificationsForUnreadCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reloadNotificationsForUnreadCount(){
|
||||||
|
List<Notification>[] 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<List<Notification>> 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<Notification> 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ import org.joinmastodon.android.events.StatusCreatedEvent;
|
||||||
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
import org.joinmastodon.android.model.CacheablePaginatedResponse;
|
||||||
import org.joinmastodon.android.model.Filter;
|
import org.joinmastodon.android.model.Filter;
|
||||||
import org.joinmastodon.android.model.Status;
|
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.GapStatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
@ -154,7 +155,7 @@ public class HomeTimelineFragment extends StatusListFragment{
|
||||||
new SaveMarkers(topPostID, null)
|
new SaveMarkers(topPostID, null)
|
||||||
.setCallback(new Callback<>(){
|
.setCallback(new Callback<>(){
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(SaveMarkers.Response result){
|
public void onSuccess(TimelineMarkers result){
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -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<tabViews.length;i++){
|
|
||||||
FrameLayout tabView=new FrameLayout(getActivity());
|
|
||||||
tabView.setId(switch(i){
|
|
||||||
case 0 -> 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<SimpleViewHolder>{
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,15 @@
|
||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Rect;
|
||||||
import android.os.Bundle;
|
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 android.view.View;
|
||||||
|
|
||||||
import com.squareup.otto.Subscribe;
|
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.Notification;
|
||||||
import org.joinmastodon.android.model.PaginatedResponse;
|
import org.joinmastodon.android.model.PaginatedResponse;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.displayitems.AccountCardStatusDisplayItem;
|
import org.joinmastodon.android.ui.OutlineProviders;
|
||||||
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.StatusDisplayItem;
|
||||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
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 org.parceler.Parcels;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import me.grishka.appkit.Nav;
|
import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
import me.grishka.appkit.utils.V;
|
|
||||||
|
|
||||||
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
|
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
|
||||||
private boolean onlyMentions;
|
private boolean onlyMentions=true;
|
||||||
private String maxID;
|
private String maxID;
|
||||||
|
private View tabBar;
|
||||||
|
private View mentionsTab, allTab;
|
||||||
|
private View endMark;
|
||||||
|
private String unreadMarker, realUnreadMarker;
|
||||||
|
private MenuItem markAllReadItem;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
setLayout(R.layout.fragment_notifications);
|
||||||
E.register(this);
|
E.register(this);
|
||||||
|
if(savedInstanceState!=null){
|
||||||
|
onlyMentions=savedInstanceState.getBoolean("onlyMentions", true);
|
||||||
|
}
|
||||||
|
setHasOptionsMenu(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -53,28 +72,29 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(Activity activity){
|
public void onAttach(Activity activity){
|
||||||
super.onAttach(activity);
|
super.onAttach(activity);
|
||||||
onlyMentions=getArguments().getBoolean("onlyMentions", false);
|
setTitle(R.string.notifications);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
||||||
String extraText=switch(n.type){
|
NotificationHeaderStatusDisplayItem titleItem;
|
||||||
case FOLLOW -> getString(R.string.user_followed_you);
|
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
|
||||||
case FOLLOW_REQUEST -> getString(R.string.user_sent_follow_request);
|
titleItem=null;
|
||||||
case MENTION, STATUS -> null;
|
}else{
|
||||||
case REBLOG -> getString(R.string.notification_boosted);
|
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
|
||||||
case FAVORITE -> getString(R.string.user_favorited);
|
if(n.status!=null){
|
||||||
case POLL -> getString(R.string.poll_ended);
|
n.status.card=null;
|
||||||
};
|
n.status.spoilerText=null;
|
||||||
HeaderStatusDisplayItem titleItem=extraText!=null ? new HeaderStatusDisplayItem(n.id, n.account, n.createdAt, this, accountID, null, extraText) : null;
|
}
|
||||||
|
}
|
||||||
if(n.status!=null){
|
if(n.status!=null){
|
||||||
ArrayList<StatusDisplayItem> 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<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, flags);
|
||||||
if(titleItem!=null)
|
if(titleItem!=null)
|
||||||
items.add(0, titleItem);
|
items.add(0, titleItem);
|
||||||
return items;
|
return items;
|
||||||
}else if(titleItem!=null){
|
}else if(titleItem!=null){
|
||||||
AccountCardStatusDisplayItem card=new AccountCardStatusDisplayItem(n.id, this, n.account);
|
return Collections.singletonList(titleItem);
|
||||||
return Arrays.asList(titleItem, card);
|
|
||||||
}else{
|
}else{
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
|
@ -90,6 +110,7 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
|
endMark.setVisibility(View.GONE);
|
||||||
AccountSessionManager.getInstance()
|
AccountSessionManager.getInstance()
|
||||||
.getAccount(accountID).getCacheController()
|
.getAccount(accountID).getCacheController()
|
||||||
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing, new SimpleCallback<>(this){
|
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing, new SimpleCallback<>(this){
|
||||||
|
@ -97,39 +118,23 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||||
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
public void onSuccess(PaginatedResponse<List<Notification>> result){
|
||||||
if(getActivity()==null)
|
if(getActivity()==null)
|
||||||
return;
|
return;
|
||||||
if(refreshing)
|
|
||||||
relationships.clear();
|
|
||||||
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
|
onDataLoaded(result.items.stream().filter(n->n.type!=null).collect(Collectors.toList()), !result.items.isEmpty());
|
||||||
Set<String> 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;
|
maxID=result.maxID;
|
||||||
|
endMark.setVisibility(result.items.isEmpty() ? View.VISIBLE : View.GONE);
|
||||||
if(offset==0 && !result.items.isEmpty()){
|
|
||||||
new SaveMarkers(null, result.items.get(0).id).exec(accountID);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onRelationshipsLoaded(){
|
protected void onShown(){
|
||||||
if(getActivity()==null)
|
super.onShown();
|
||||||
return;
|
unreadMarker=realUnreadMarker=AccountSessionManager.get(accountID).getLastKnownNotificationsMarker();
|
||||||
for(int i=0;i<list.getChildCount();i++){
|
|
||||||
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
|
|
||||||
if(holder instanceof AccountCardStatusDisplayItem.Holder accountHolder)
|
|
||||||
accountHolder.rebind();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onShown(){
|
protected void onHidden(){
|
||||||
super.onShown();
|
super.onHidden();
|
||||||
// if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
|
resetUnreadBackground();
|
||||||
// loadData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -153,8 +158,62 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
tabBar=view.findViewById(R.id.tabbar);
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||||
|
|
||||||
|
View tabBarItself=view.findViewById(R.id.tabbar_inner);
|
||||||
|
tabBarItself.setOutlineProvider(OutlineProviders.roundedRect(20));
|
||||||
|
tabBarItself.setClipToOutline(true);
|
||||||
|
|
||||||
|
mentionsTab=view.findViewById(R.id.mentions_tab);
|
||||||
|
allTab=view.findViewById(R.id.all_tab);
|
||||||
|
mentionsTab.setOnClickListener(this::onTabClick);
|
||||||
|
allTab.setOnClickListener(this::onTabClick);
|
||||||
|
mentionsTab.setSelected(onlyMentions);
|
||||||
|
allTab.setSelected(!onlyMentions);
|
||||||
|
|
||||||
|
NestedRecyclerScrollView scroller=view.findViewById(R.id.scroller);
|
||||||
|
scroller.setScrollableChildSupplier(()->list);
|
||||||
|
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<parent.getChildCount();i++){
|
||||||
|
View child=parent.getChildAt(i);
|
||||||
|
if(parent.getChildViewHolder(child) instanceof StatusDisplayItem.Holder<?> holder){
|
||||||
|
String itemID=holder.getItemID();
|
||||||
|
if(ObjectIdComparator.INSTANCE.compare(itemID, unreadMarker)>0){
|
||||||
|
parent.getDecoratedBoundsWithMargins(child, tmpRect);
|
||||||
|
c.drawRect(tmpRect, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<View> getViewsForElevationEffect(){
|
||||||
|
ArrayList<View> 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){
|
private Notification getNotificationByID(String id){
|
||||||
|
@ -211,4 +270,85 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||||
displayItems.subList(index, lastIndex).clear();
|
displayItems.subList(index, lastIndex).clear();
|
||||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void onTabClick(View v){
|
||||||
|
boolean newOnlyMentions=v.getId()==R.id.mentions_tab;
|
||||||
|
if(newOnlyMentions==onlyMentions)
|
||||||
|
return;
|
||||||
|
onlyMentions=newOnlyMentions;
|
||||||
|
mentionsTab.setSelected(onlyMentions);
|
||||||
|
allTab.setSelected(!onlyMentions);
|
||||||
|
maxID=null;
|
||||||
|
showProgress();
|
||||||
|
loadData(0, 20);
|
||||||
|
refreshing=true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected View onCreateFooterView(LayoutInflater inflater){
|
||||||
|
View v=inflater.inflate(R.layout.load_more_with_end_mark, null);
|
||||||
|
endMark=v.findViewById(R.id.end_mark);
|
||||||
|
endMark.setVisibility(View.GONE);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
||||||
|
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=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<Notification> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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+
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -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<AccountCardStatusDisplayItem> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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<NotificationHeaderStatusDisplayItem> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ import org.joinmastodon.android.model.Poll;
|
||||||
import org.joinmastodon.android.ui.OutlineProviders;
|
import org.joinmastodon.android.ui.OutlineProviders;
|
||||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
@ -65,7 +66,7 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||||
public static class Holder extends StatusDisplayItem.Holder<PollOptionStatusDisplayItem> implements ImageLoaderViewHolder{
|
public static class Holder extends StatusDisplayItem.Holder<PollOptionStatusDisplayItem> implements ImageLoaderViewHolder{
|
||||||
private final TextView text, percent;
|
private final TextView text, percent;
|
||||||
private final View check, button;
|
private final View check, button;
|
||||||
private final Drawable progressBg;
|
private final Drawable progressBg, progressBgInset;
|
||||||
|
|
||||||
public Holder(Activity activity, ViewGroup parent){
|
public Holder(Activity activity, ViewGroup parent){
|
||||||
super(activity, R.layout.display_item_poll_option, parent);
|
super(activity, R.layout.display_item_poll_option, parent);
|
||||||
|
@ -74,6 +75,7 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||||
check=findViewById(R.id.checkbox);
|
check=findViewById(R.id.checkbox);
|
||||||
button=findViewById(R.id.button);
|
button=findViewById(R.id.button);
|
||||||
progressBg=activity.getResources().getDrawable(R.drawable.bg_poll_option_voted, activity.getTheme()).mutate();
|
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);
|
itemView.setOnClickListener(this::onButtonClick);
|
||||||
button.setOutlineProvider(OutlineProviders.roundedRect(20));
|
button.setOutlineProvider(OutlineProviders.roundedRect(20));
|
||||||
button.setClipToOutline(true);
|
button.setClipToOutline(true);
|
||||||
|
@ -85,13 +87,21 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||||
percent.setVisibility(item.showResults ? View.VISIBLE : View.GONE);
|
percent.setVisibility(item.showResults ? View.VISIBLE : View.GONE);
|
||||||
itemView.setClickable(!item.showResults);
|
itemView.setClickable(!item.showResults);
|
||||||
if(item.showResults){
|
if(item.showResults){
|
||||||
progressBg.setLevel(Math.round(10000f*item.votesFraction));
|
Drawable bg=item.inset ? progressBgInset : progressBg;
|
||||||
button.setBackground(progressBg);
|
bg.setLevel(Math.round(10000f*item.votesFraction));
|
||||||
|
button.setBackground(bg);
|
||||||
itemView.setSelected(item.isMostVoted);
|
itemView.setSelected(item.isMostVoted);
|
||||||
percent.setText(String.format(Locale.getDefault(), "%d%%", Math.round(item.votesFraction*100f)));
|
percent.setText(String.format(Locale.getDefault(), "%d%%", Math.round(item.votesFraction*100f)));
|
||||||
}else{
|
}else{
|
||||||
itemView.setSelected(item.poll.selectedOptions!=null && item.poll.selectedOptions.contains(item.option));
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,6 +37,7 @@ public abstract class StatusDisplayItem{
|
||||||
public static final int FLAG_NO_FOOTER=1 << 1;
|
public static final int FLAG_NO_FOOTER=1 << 1;
|
||||||
public static final int FLAG_CHECKABLE=1 << 2;
|
public static final int FLAG_CHECKABLE=1 << 2;
|
||||||
public static final int FLAG_MEDIA_FORCE_HIDDEN=1 << 3;
|
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){
|
public StatusDisplayItem(String parentID, BaseStatusListFragment parentFragment){
|
||||||
this.parentID=parentID;
|
this.parentID=parentID;
|
||||||
|
@ -64,7 +65,6 @@ public abstract class StatusDisplayItem{
|
||||||
case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent);
|
case POLL_FOOTER -> new PollFooterStatusDisplayItem.Holder(activity, parent);
|
||||||
case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent);
|
case CARD -> new LinkCardStatusDisplayItem.Holder(activity, parent);
|
||||||
case FOOTER -> new FooterStatusDisplayItem.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 ACCOUNT -> new AccountStatusDisplayItem.Holder(activity, parent);
|
||||||
case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
|
case HASHTAG -> new HashtagStatusDisplayItem.Holder(activity, parent);
|
||||||
case GAP -> new GapStatusDisplayItem.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 MEDIA_GRID -> new MediaGridStatusDisplayItem.Holder(activity, parent);
|
||||||
case SPOILER -> new SpoilerStatusDisplayItem.Holder(activity, parent);
|
case SPOILER -> new SpoilerStatusDisplayItem.Holder(activity, parent);
|
||||||
case SECTION_HEADER -> new SectionHeaderStatusDisplayItem.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();
|
String parentID=parentObject.getID();
|
||||||
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
ArrayList<StatusDisplayItem> items=new ArrayList<>();
|
||||||
Status statusForContent=status.getContentStatus();
|
Status statusForContent=status.getContentStatus();
|
||||||
if(status.reblog!=null){
|
HeaderStatusDisplayItem header=null;
|
||||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_repeat_20px));
|
if((flags & FLAG_NO_HEADER)==0){
|
||||||
}else if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId)){
|
if(status.reblog!=null){
|
||||||
Account account=Objects.requireNonNull(knownAccounts.get(status.inReplyToAccountId));
|
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.user_boosted, status.account.displayName), status.account.emojis, R.drawable.ic_repeat_20px));
|
||||||
items.add(new ReblogOrReplyLineStatusDisplayItem(parentID, fragment, fragment.getString(R.string.in_reply_to, account.displayName), account.emojis, R.drawable.ic_reply_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<StatusDisplayItem> contentItems;
|
ArrayList<StatusDisplayItem> contentItems;
|
||||||
if(!TextUtils.isEmpty(statusForContent.spoilerText)){
|
if(!TextUtils.isEmpty(statusForContent.spoilerText)){
|
||||||
|
@ -109,10 +112,13 @@ public abstract class StatusDisplayItem{
|
||||||
contentItems=items;
|
contentItems=items;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!TextUtils.isEmpty(statusForContent.content))
|
if(!TextUtils.isEmpty(statusForContent.content)){
|
||||||
contentItems.add(new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent));
|
TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent);
|
||||||
else
|
text.reduceTopPadding=header==null;
|
||||||
|
contentItems.add(text);
|
||||||
|
}else if(header!=null){
|
||||||
header.needBottomPadding=true;
|
header.needBottomPadding=true;
|
||||||
|
}
|
||||||
|
|
||||||
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
|
List<Attachment> imageAttachments=statusForContent.mediaAttachments.stream().filter(att->att.type.isImage()).collect(Collectors.toList());
|
||||||
if(!imageAttachments.isEmpty()){
|
if(!imageAttachments.isEmpty()){
|
||||||
|
@ -171,7 +177,6 @@ public abstract class StatusDisplayItem{
|
||||||
POLL_FOOTER,
|
POLL_FOOTER,
|
||||||
CARD,
|
CARD,
|
||||||
FOOTER,
|
FOOTER,
|
||||||
ACCOUNT_CARD,
|
|
||||||
ACCOUNT,
|
ACCOUNT,
|
||||||
HASHTAG,
|
HASHTAG,
|
||||||
GAP,
|
GAP,
|
||||||
|
@ -179,7 +184,8 @@ public abstract class StatusDisplayItem{
|
||||||
MEDIA_GRID,
|
MEDIA_GRID,
|
||||||
SPOILER,
|
SPOILER,
|
||||||
SECTION_HEADER,
|
SECTION_HEADER,
|
||||||
HEADER_CHECKABLE
|
HEADER_CHECKABLE,
|
||||||
|
NOTIFICATION_HEADER
|
||||||
}
|
}
|
||||||
|
|
||||||
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
|
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
|
||||||
|
|
|
@ -9,16 +9,19 @@ import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||||
import org.joinmastodon.android.model.Status;
|
import org.joinmastodon.android.model.Status;
|
||||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import org.joinmastodon.android.ui.views.LinkedTextView;
|
import org.joinmastodon.android.ui.views.LinkedTextView;
|
||||||
|
|
||||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||||
import me.grishka.appkit.imageloader.MovieDrawable;
|
import me.grishka.appkit.imageloader.MovieDrawable;
|
||||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||||
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class TextStatusDisplayItem extends StatusDisplayItem{
|
public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||||
private CharSequence text;
|
private CharSequence text;
|
||||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||||
public boolean textSelectable;
|
public boolean textSelectable;
|
||||||
|
public boolean reduceTopPadding;
|
||||||
public final Status status;
|
public final Status status;
|
||||||
|
|
||||||
public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, 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.setTextIsSelectable(item.textSelectable);
|
||||||
text.setInvalidateOnEveryFrame(false);
|
text.setInvalidateOnEveryFrame(false);
|
||||||
itemView.setClickable(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
|
@Override
|
||||||
|
|
|
@ -28,8 +28,8 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
|
||||||
|
|
||||||
public InsetStatusItemDecoration(BaseStatusListFragment<?> listFragment){
|
public InsetStatusItemDecoration(BaseStatusListFragment<?> listFragment){
|
||||||
this.listFragment=listFragment;
|
this.listFragment=listFragment;
|
||||||
bgColor=UiUtils.getThemeColor(listFragment.getActivity(), android.R.attr.colorBackground);
|
bgColor=UiUtils.getThemeColor(listFragment.getActivity(), R.attr.colorM3SurfaceVariant);
|
||||||
borderColor=UiUtils.getThemeColor(listFragment.getActivity(), R.attr.colorPollVoted);
|
borderColor=UiUtils.getThemeColor(listFragment.getActivity(), R.attr.colorM3OutlineVariant);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -64,9 +64,8 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
|
||||||
private void drawInsetBackground(RecyclerView list, Canvas c){
|
private void drawInsetBackground(RecyclerView list, Canvas c){
|
||||||
paint.setStyle(Paint.Style.FILL);
|
paint.setStyle(Paint.Style.FILL);
|
||||||
paint.setColor(bgColor);
|
paint.setColor(bgColor);
|
||||||
rect.left=V.dp(12);
|
rect.left=V.dp(16);
|
||||||
rect.right=list.getWidth()-V.dp(12);
|
rect.right=list.getWidth()-V.dp(16);
|
||||||
rect.inset(V.dp(4), V.dp(4));
|
|
||||||
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
|
c.drawRoundRect(rect, V.dp(4), V.dp(4), paint);
|
||||||
paint.setStyle(Paint.Style.STROKE);
|
paint.setStyle(Paint.Style.STROKE);
|
||||||
paint.setStrokeWidth(V.dp(1));
|
paint.setStrokeWidth(V.dp(1));
|
||||||
|
@ -85,20 +84,15 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
|
||||||
if(inset){
|
if(inset){
|
||||||
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
|
boolean topSiblingInset=pos>0 && displayItems.get(pos-1).inset;
|
||||||
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
|
boolean bottomSiblingInset=pos<displayItems.size()-1 && displayItems.get(pos+1).inset;
|
||||||
int pad;
|
StatusDisplayItem.Type type=sdi.getItem().getType();
|
||||||
if(holder instanceof MediaGridStatusDisplayItem.Holder || holder instanceof LinkCardStatusDisplayItem.Holder)
|
if(type==StatusDisplayItem.Type.CARD || type==StatusDisplayItem.Type.MEDIA_GRID)
|
||||||
pad=V.dp(16);
|
outRect.left=outRect.right=V.dp(16);
|
||||||
else
|
else
|
||||||
pad=V.dp(12);
|
outRect.left=outRect.right=V.dp(8);
|
||||||
boolean insetLeft=true, insetRight=true;
|
|
||||||
if(insetLeft)
|
|
||||||
outRect.left=pad;
|
|
||||||
if(insetRight)
|
|
||||||
outRect.right=pad;
|
|
||||||
if(!topSiblingInset)
|
|
||||||
outRect.top=pad;
|
|
||||||
if(!bottomSiblingInset)
|
if(!bottomSiblingInset)
|
||||||
outRect.bottom=pad;
|
outRect.bottom=V.dp(16);
|
||||||
|
if(!topSiblingInset)
|
||||||
|
outRect.top=V.dp(-8);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,6 @@ package org.joinmastodon.android.ui.views;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.widget.Button;
|
|
||||||
|
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
@ -11,9 +10,7 @@ import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
|
|
||||||
public class FilterChipView extends Button{
|
public class FilterChipView extends CheckIconSelectableTextView{
|
||||||
|
|
||||||
private boolean currentlySelected;
|
|
||||||
|
|
||||||
public FilterChipView(Context context){
|
public FilterChipView(Context context){
|
||||||
this(context, null);
|
this(context, null);
|
||||||
|
@ -35,14 +32,6 @@ public class FilterChipView extends Button{
|
||||||
@Override
|
@Override
|
||||||
protected void drawableStateChanged(){
|
protected void drawableStateChanged(){
|
||||||
super.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);
|
|
||||||
updatePadding();
|
updatePadding();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
public class NestedRecyclerScrollView extends CustomScrollView{
|
public class NestedRecyclerScrollView extends CustomScrollView{
|
||||||
private Supplier<RecyclerView> scrollableChildSupplier;
|
private Supplier<RecyclerView> scrollableChildSupplier;
|
||||||
|
private boolean takePriorityOverChildViews;
|
||||||
|
|
||||||
public NestedRecyclerScrollView(Context context){
|
public NestedRecyclerScrollView(Context context){
|
||||||
super(context);
|
super(context);
|
||||||
|
@ -25,32 +26,43 @@ public class NestedRecyclerScrollView extends CustomScrollView{
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
|
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed){
|
||||||
if(target instanceof RecyclerView rv && ((dy < 0 && isScrolledToTop(rv)) || (dy > 0 && !isScrolledToBottom()))){
|
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);
|
scrollBy(0, dy);
|
||||||
consumed[1] = dy;
|
consumed[1]=dy;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
super.onNestedPreScroll(target, dx, dy, consumed);
|
super.onNestedPreScroll(target, dx, dy, consumed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onNestedPreFling(View target, float velX, float velY) {
|
public boolean onNestedPreFling(View target, float velX, float velY){
|
||||||
if (target instanceof RecyclerView rv && ((velY < 0 && isScrolledToTop(rv)) || (velY > 0 && !isScrolledToBottom()))){
|
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);
|
fling((int) velY);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return super.onNestedPreFling(target, velX, velY);
|
return super.onNestedPreFling(target, velX, velY);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isScrolledToBottom() {
|
private boolean isScrolledToBottom(){
|
||||||
return !canScrollVertically(1);
|
return !canScrollVertically(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isScrolledToTop(RecyclerView rv) {
|
private boolean isScrolledToTop(RecyclerView rv){
|
||||||
final LinearLayoutManager lm = (LinearLayoutManager) rv.getLayoutManager();
|
final LinearLayoutManager lm=(LinearLayoutManager) rv.getLayoutManager();
|
||||||
return lm.findFirstVisibleItemPosition() == 0
|
return lm.findFirstVisibleItemPosition()==0
|
||||||
&& lm.findViewByPosition(0).getTop() == rv.getPaddingTop();
|
&& lm.findViewByPosition(0).getTop()==rv.getPaddingTop();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setScrollableChildSupplier(Supplier<RecyclerView> scrollableChildSupplier){
|
public void setScrollableChildSupplier(Supplier<RecyclerView> scrollableChildSupplier){
|
||||||
|
@ -59,12 +71,20 @@ public class NestedRecyclerScrollView extends CustomScrollView{
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean onScrollingHitEdge(float velocity){
|
protected boolean onScrollingHitEdge(float velocity){
|
||||||
if(velocity>0){
|
if(velocity>0 || takePriorityOverChildViews){
|
||||||
RecyclerView view=scrollableChildSupplier.get();
|
RecyclerView view=scrollableChildSupplier.get();
|
||||||
if(view!=null){
|
if(view!=null){
|
||||||
return view.fling(0, (int)velocity);
|
return view.fling(0, (int) velocity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isTakePriorityOverChildViews(){
|
||||||
|
return takePriorityOverChildViews;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTakePriorityOverChildViews(boolean takePriorityOverChildViews){
|
||||||
|
this.takePriorityOverChildViews=takePriorityOverChildViews;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<getChildCount()-1;i++){
|
||||||
|
topBarsHeight+=getChildAt(i).getMeasuredHeight();
|
||||||
|
}
|
||||||
|
super.onMeasure(widthMeasureSpec, (MeasureSpec.getSize(heightMeasureSpec)+topBarsHeight) | MeasureSpec.EXACTLY);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package org.joinmastodon.android.utils;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
|
||||||
|
public class ObjectIdComparator implements Comparator<String>{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="?colorM3TertiaryContainer" android:state_selected="true"/>
|
||||||
|
<item android:color="?colorM3SurfaceVariant"/>
|
||||||
|
</selector>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="?colorM3OnTertiaryContainer" android:state_selected="true"/>
|
||||||
|
<item android:color="?colorM3OnSurface"/>
|
||||||
|
</selector>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="?colorM3OnSecondaryContainer" android:state_selected="true"/>
|
||||||
|
<item android:color="?colorM3OnSurface"/>
|
||||||
|
</selector>
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="?android:colorControlHighlight">
|
||||||
|
<item>
|
||||||
|
<shape>
|
||||||
|
<stroke android:width="1dp" android:color="?colorM3Outline"/>
|
||||||
|
<solid android:color="?colorM3Surface"/>
|
||||||
|
<corners android:radius="20dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item android:id="@android:id/mask">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="#000"/>
|
||||||
|
<corners android:radius="20dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape>
|
||||||
|
<solid android:color="?colorM3Surface"/>
|
||||||
|
<corners android:radius="20dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<scale android:scaleGravity="start|fill_vertical" android:scaleWidth="100%">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/poll_option_progress_inset"/>
|
||||||
|
<corners android:radius="20dp"/>
|
||||||
|
</shape>
|
||||||
|
</scale>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<shape>
|
||||||
|
<stroke android:width="1dp" android:color="?colorM3Outline"/>
|
||||||
|
<corners android:radius="20dp"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</layer-list>
|
|
@ -0,0 +1,17 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple xmlns:android="http://schemas.android.com/apk/res/android" android:color="@color/m3_on_surface_overlay">
|
||||||
|
<item android:id="@android:id/mask">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="#000"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<selector>
|
||||||
|
<item android:state_selected="true">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="?colorM3SecondaryContainer"/>
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</selector>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="?colorM3Error"/>
|
||||||
|
<corners android:radius="8dp"/>
|
||||||
|
</shape>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<size android:width="1dp"/>
|
||||||
|
<solid android:color="?colorM3Outline"/>
|
||||||
|
</shape>
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<corners android:radius="20dp"/>
|
||||||
|
<stroke android:color="?colorM3Outline" android:width="1dp"/>
|
||||||
|
</shape>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6.7,18 L1.05,12.35 2.475,10.95 6.725,15.2 8.125,16.6ZM12.35,18 L6.7,12.35 8.1,10.925 12.35,15.175 21.55,5.975 22.95,7.4ZM12.35,12.35 L10.925,10.95 15.875,6 17.3,7.4Z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M18,14V11H15V9H18V6H20V9H23V11H20V14ZM9,12Q7.35,12 6.175,10.825Q5,9.65 5,8Q5,6.35 6.175,5.175Q7.35,4 9,4Q10.65,4 11.825,5.175Q13,6.35 13,8Q13,9.65 11.825,10.825Q10.65,12 9,12ZM1,20V17.2Q1,16.35 1.438,15.637Q1.875,14.925 2.6,14.55Q4.15,13.775 5.75,13.387Q7.35,13 9,13Q10.65,13 12.25,13.387Q13.85,13.775 15.4,14.55Q16.125,14.925 16.562,15.637Q17,16.35 17,17.2V20Z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M7,22 L3,18 7,14 8.4,15.45 6.85,17H17V13H19V19H6.85L8.4,20.55ZM5,11V5H17.15L15.6,3.45L17,2L21,6L17,10L15.6,8.55L17.15,7H7V11Z"/>
|
||||||
|
</vector>
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M5.825,22 L8.15,14.4 2,10H9.6L12,2L14.4,10H22L15.85,14.4L18.175,22L12,17.3Z"/>
|
||||||
|
</vector>
|
|
@ -1,12 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
|
||||||
<include layout="@layout/item_discover_account"
|
|
||||||
android:id="@+id/card"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_margin="16dp"/>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dp"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:paddingHorizontal="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/icon"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
tools:tint="#0f0"
|
||||||
|
tools:src="@drawable/ic_repeat_24px"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/avatar"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginStart="8dp"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:textAppearance="@style/m3_body_large"
|
||||||
|
android:textColor="?colorM3OnSurface"
|
||||||
|
android:singleLine="true"
|
||||||
|
tools:text="Notification text"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -1,25 +1,84 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<me.grishka.appkit.views.FragmentRootLinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_width="match_parent"
|
android:id="@+id/appkit_loader_root"
|
||||||
android:layout_height="match_parent">
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:background="?colorM3Surface">
|
||||||
|
|
||||||
<org.joinmastodon.android.ui.tabs.TabLayout
|
<org.joinmastodon.android.ui.views.NestedRecyclerScrollView
|
||||||
android:id="@+id/tabbar"
|
android:id="@+id/scroller"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="48dp"
|
android:layout_height="match_parent"
|
||||||
app:tabGravity="fill"
|
android:fillViewport="true">
|
||||||
app:tabIndicator="@drawable/mtrl_tabs_default_indicator"
|
|
||||||
app:tabIndicatorAnimationMode="elastic"
|
|
||||||
app:tabIndicatorColor="?android:textColorPrimary"
|
|
||||||
app:tabMode="fixed"
|
|
||||||
android:background="@drawable/bg_discover_tabs"/>
|
|
||||||
|
|
||||||
<androidx.viewpager2.widget.ViewPager2
|
<org.joinmastodon.android.ui.views.TopBarsScrollAwayLinearLayout
|
||||||
android:id="@+id/pager"
|
android:layout_width="match_parent"
|
||||||
android:layout_width="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_height="0dp"
|
android:orientation="vertical">
|
||||||
android:layout_weight="1"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
<include layout="@layout/appkit_toolbar"/>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/tabbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="64dp">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/tabbar_inner"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
style="@style/Widget.Mastodon.M3.SegmentedButtonContainer">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/mentions_tab"
|
||||||
|
style="@style/Widget.Mastodon.M3.SegmentedButton">
|
||||||
|
<org.joinmastodon.android.ui.views.CheckIconSelectableTextView
|
||||||
|
android:id="@+id/mentions_text"
|
||||||
|
style="@style/Widget.Mastodon.M3.SegmentedButtonText"
|
||||||
|
android:text="@string/mentions"/>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/all_tab"
|
||||||
|
style="@style/Widget.Mastodon.M3.SegmentedButton">
|
||||||
|
<org.joinmastodon.android.ui.views.CheckIconSelectableTextView
|
||||||
|
android:id="@+id/all_text"
|
||||||
|
style="@style/Widget.Mastodon.M3.SegmentedButtonText"
|
||||||
|
android:text="@string/all_notifications"/>
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/appkit_loader_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<include layout="@layout/loading"
|
||||||
|
android:id="@+id/loading"/>
|
||||||
|
|
||||||
|
<ViewStub android:layout="?errorViewLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:id="@+id/error"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:id="@+id/content_stub"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
</org.joinmastodon.android.ui.views.TopBarsScrollAwayLinearLayout>
|
||||||
|
</org.joinmastodon.android.ui.views.NestedRecyclerScrollView>
|
||||||
|
|
||||||
|
</me.grishka.appkit.views.FragmentRootLinearLayout>
|
|
@ -0,0 +1,51 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="76dip">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/load_more_progress"
|
||||||
|
android:layout_width="32dip"
|
||||||
|
android:layout_height="32dip"
|
||||||
|
android:layout_margin="22dip"
|
||||||
|
android:layout_gravity="center"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/load_more_error"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="76dip"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="16dip">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/error_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:textSize="14dip"
|
||||||
|
android:textColor="#71757A"
|
||||||
|
android:text="Some error text"
|
||||||
|
android:singleLine="true"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/error_retry"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:text="Retry?"
|
||||||
|
android:textAllCaps="true"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:padding="12dip"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/end_mark"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="9dp"
|
||||||
|
android:layout_marginVertical="16dp"
|
||||||
|
android:layout_gravity="center_horizontal"
|
||||||
|
android:background="@drawable/thread_end_mark"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
|
@ -73,6 +73,22 @@
|
||||||
android:importantForAccessibility="no"
|
android:importantForAccessibility="no"
|
||||||
android:tint="?colorM3OnSurfaceVariant"
|
android:tint="?colorM3OnSurfaceVariant"
|
||||||
android:src="@drawable/ic_notifications_24px"/>
|
android:src="@drawable/ic_notifications_24px"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/notifications_badge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:layout_marginTop="-6dp"
|
||||||
|
android:layout_marginEnd="-8dp"
|
||||||
|
android:background="@drawable/bg_tabbar_badge"
|
||||||
|
android:textColor="?colorM3OnPrimary"
|
||||||
|
android:gravity="center"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:textAppearance="@style/m3_label_small"
|
||||||
|
android:minWidth="16dp"
|
||||||
|
android:paddingHorizontal="4dp"
|
||||||
|
tools:text="222"/>
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:id="@+id/mark_all_read" android:icon="@drawable/ic_done_all_24px" android:title="@string/mark_all_notifications_read" android:showAsAction="always"/>
|
||||||
|
</menu>
|
|
@ -45,6 +45,10 @@
|
||||||
<attr name="colorM3PrimaryInverse" format="color"/>
|
<attr name="colorM3PrimaryInverse" format="color"/>
|
||||||
<attr name="colorSensitiveOverlay" format="color"/>
|
<attr name="colorSensitiveOverlay" format="color"/>
|
||||||
<attr name="colorWhite" format="color"/>
|
<attr name="colorWhite" format="color"/>
|
||||||
|
<attr name="colorFavorite" format="color" />
|
||||||
|
<attr name="colorBoost" format="color" />
|
||||||
|
<attr name="colorFollow" format="color" />
|
||||||
|
<attr name="colorPoll" format="color" />
|
||||||
|
|
||||||
<attr name="primaryLargeButtonStyle" format="reference"/>
|
<attr name="primaryLargeButtonStyle" format="reference"/>
|
||||||
<attr name="secondaryLargeButtonStyle" format="reference"/>
|
<attr name="secondaryLargeButtonStyle" format="reference"/>
|
||||||
|
|
|
@ -16,11 +16,11 @@
|
||||||
<string name="in_reply_to">In reply to %s</string>
|
<string name="in_reply_to">In reply to %s</string>
|
||||||
<string name="notifications">Notifications</string>
|
<string name="notifications">Notifications</string>
|
||||||
|
|
||||||
<string name="user_followed_you">followed you</string>
|
<string name="user_followed_you">%s followed you</string>
|
||||||
<string name="user_sent_follow_request">sent you a follow request</string>
|
<string name="user_sent_follow_request">%s sent you a follow request</string>
|
||||||
<string name="user_favorited">favorited your post</string>
|
<string name="user_favorited">%s favorited your post</string>
|
||||||
<string name="notification_boosted">boosted your post</string>
|
<string name="notification_boosted">%s boosted your post</string>
|
||||||
<string name="poll_ended">poll ended</string>
|
<string name="poll_ended">See the results of a poll you voted in</string>
|
||||||
|
|
||||||
<string name="time_seconds">%ds</string>
|
<string name="time_seconds">%ds</string>
|
||||||
<string name="time_minutes">%dm</string>
|
<string name="time_minutes">%dm</string>
|
||||||
|
@ -139,7 +139,7 @@
|
||||||
<string name="hashtags">Hashtags</string>
|
<string name="hashtags">Hashtags</string>
|
||||||
<string name="news">News</string>
|
<string name="news">News</string>
|
||||||
<string name="for_you">For you</string>
|
<string name="for_you">For you</string>
|
||||||
<string name="all_notifications">All</string>
|
<string name="all_notifications">Everything</string>
|
||||||
<string name="mentions">Mentions</string>
|
<string name="mentions">Mentions</string>
|
||||||
<plurals name="x_people_talking">
|
<plurals name="x_people_talking">
|
||||||
<item quantity="one">%d person is talking</item>
|
<item quantity="one">%d person is talking</item>
|
||||||
|
@ -502,4 +502,5 @@
|
||||||
<string name="report_sent_already_blocked">You’ve already blocked this user, so there’s nothing else you need to do while we review your report.</string>
|
<string name="report_sent_already_blocked">You’ve already blocked this user, so there’s nothing else you need to do while we review your report.</string>
|
||||||
<string name="report_personal_already_blocked">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!</string>
|
<string name="report_personal_already_blocked">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!</string>
|
||||||
<string name="blocked_user">Blocked %s</string>
|
<string name="blocked_user">Blocked %s</string>
|
||||||
|
<string name="mark_all_notifications_read">Mark all as read</string>
|
||||||
</resources>
|
</resources>
|
|
@ -68,6 +68,10 @@
|
||||||
<item name="colorM3OnErrorContainer">#410E0B</item>
|
<item name="colorM3OnErrorContainer">#410E0B</item>
|
||||||
<item name="colorM3PrimaryInverse">@color/m3_sys_dark_primary</item>
|
<item name="colorM3PrimaryInverse">@color/m3_sys_dark_primary</item>
|
||||||
<item name="colorWhite">#FFF</item>
|
<item name="colorWhite">#FFF</item>
|
||||||
|
<item name="colorFavorite">#8b5000</item>
|
||||||
|
<item name="colorBoost">#ab332a</item>
|
||||||
|
<item name="colorFollow">#4746e3</item>
|
||||||
|
<item name="colorPoll">#006d42</item>
|
||||||
|
|
||||||
<item name="colorWindowBackground">?colorM3Background</item>
|
<item name="colorWindowBackground">?colorM3Background</item>
|
||||||
<item name="android:statusBarColor">?colorM3Background</item>
|
<item name="android:statusBarColor">?colorM3Background</item>
|
||||||
|
@ -146,6 +150,10 @@
|
||||||
<item name="colorM3OnErrorContainer">#F9DEDC</item>
|
<item name="colorM3OnErrorContainer">#F9DEDC</item>
|
||||||
<item name="colorM3PrimaryInverse">@color/m3_sys_light_primary</item>
|
<item name="colorM3PrimaryInverse">@color/m3_sys_light_primary</item>
|
||||||
<item name="colorWhite">#000</item>
|
<item name="colorWhite">#000</item>
|
||||||
|
<item name="colorFavorite">#ffb871</item>
|
||||||
|
<item name="colorBoost">#ffb4aa</item>
|
||||||
|
<item name="colorFollow">#c1c1ff</item>
|
||||||
|
<item name="colorPoll">#77daa1</item>
|
||||||
|
|
||||||
<item name="colorWindowBackground">?colorM3Background</item>
|
<item name="colorWindowBackground">?colorM3Background</item>
|
||||||
<item name="android:statusBarColor">?colorM3Background</item>
|
<item name="android:statusBarColor">?colorM3Background</item>
|
||||||
|
@ -357,6 +365,30 @@
|
||||||
<item name="android:textColor">?colorM3OnSurface</item>
|
<item name="android:textColor">?colorM3OnSurface</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Mastodon.M3.SegmentedButtonContainer" parent="">
|
||||||
|
<item name="android:divider">@drawable/divider_vertical_outline</item>
|
||||||
|
<item name="android:showDividers">middle</item>
|
||||||
|
<item name="android:foreground">@drawable/fg_segmented_button_container</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Mastodon.M3.SegmentedButton" parent="">
|
||||||
|
<item name="android:layout_width">0dp</item>
|
||||||
|
<item name="android:layout_height">match_parent</item>
|
||||||
|
<item name="android:layout_weight">1</item>
|
||||||
|
<item name="android:background">@drawable/bg_segmented_button</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Mastodon.M3.SegmentedButtonText" parent="">
|
||||||
|
<item name="android:textAppearance">@style/m3_label_large</item>
|
||||||
|
<item name="android:layout_width">wrap_content</item>
|
||||||
|
<item name="android:layout_height">match_parent</item>
|
||||||
|
<item name="android:layout_gravity">center_horizontal</item>
|
||||||
|
<item name="android:gravity">center_vertical</item>
|
||||||
|
<item name="android:drawablePadding">8dp</item>
|
||||||
|
<item name="android:textColor">@color/text_segmented_button</item>
|
||||||
|
<item name="android:duplicateParentState">true</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="Theme.Mastodon.Toolbar.ProgressBar">
|
<style name="Theme.Mastodon.Toolbar.ProgressBar">
|
||||||
<item name="android:disabledAlpha">0</item>
|
<item name="android:disabledAlpha">0</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in New Issue