Notification requests (AND-154)
This commit is contained in:
parent
f888091e22
commit
441567f9d2
34 changed files with 1053 additions and 122 deletions
|
@ -13,7 +13,7 @@ android {
|
||||||
applicationId "org.joinmastodon.android"
|
applicationId "org.joinmastodon.android"
|
||||||
minSdk 23
|
minSdk 23
|
||||||
targetSdk 33
|
targetSdk 33
|
||||||
versionCode 90
|
versionCode 92
|
||||||
versionName "2.4.1"
|
versionName "2.4.1"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.joinmastodon.android.api.requests.notifications;
|
||||||
|
|
||||||
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.requests.HeaderPaginationRequest;
|
||||||
|
import org.joinmastodon.android.model.NotificationRequest;
|
||||||
|
|
||||||
|
public class GetNotificationRequests extends HeaderPaginationRequest<NotificationRequest>{
|
||||||
|
public GetNotificationRequests(String maxID){
|
||||||
|
super(HttpMethod.GET, "/notifications/requests", new TypeToken<>(){});
|
||||||
|
if(maxID!=null)
|
||||||
|
addQueryParameter("max_id", maxID);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package org.joinmastodon.android.api.requests.notifications;
|
package org.joinmastodon.android.api.requests.notifications;
|
||||||
|
|
||||||
import com.google.gson.annotations.SerializedName;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
import com.google.gson.reflect.TypeToken;
|
||||||
|
|
||||||
import org.joinmastodon.android.api.ApiUtils;
|
import org.joinmastodon.android.api.ApiUtils;
|
||||||
|
@ -12,6 +13,10 @@ import java.util.List;
|
||||||
|
|
||||||
public class GetNotifications extends MastodonAPIRequest<List<Notification>>{
|
public class GetNotifications extends MastodonAPIRequest<List<Notification>>{
|
||||||
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes){
|
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes){
|
||||||
|
this(maxID, limit, includeTypes, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GetNotifications(String maxID, int limit, EnumSet<Notification.Type> includeTypes, String onlyAccountID){
|
||||||
super(HttpMethod.GET, "/notifications", new TypeToken<>(){});
|
super(HttpMethod.GET, "/notifications", new TypeToken<>(){});
|
||||||
if(maxID!=null)
|
if(maxID!=null)
|
||||||
addQueryParameter("max_id", maxID);
|
addQueryParameter("max_id", maxID);
|
||||||
|
@ -25,6 +30,8 @@ public class GetNotifications extends MastodonAPIRequest<List<Notification>>{
|
||||||
addQueryParameter("exclude_types[]", type);
|
addQueryParameter("exclude_types[]", type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(!TextUtils.isEmpty(onlyAccountID))
|
||||||
|
addQueryParameter("account_id", onlyAccountID);
|
||||||
removeUnsupportedItems=true;
|
removeUnsupportedItems=true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package org.joinmastodon.android.api.requests.notifications;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.NotificationsPolicy;
|
||||||
|
|
||||||
|
public class GetNotificationsPolicy extends MastodonAPIRequest<NotificationsPolicy>{
|
||||||
|
public GetNotificationsPolicy(){
|
||||||
|
super(HttpMethod.GET, "/notifications/policy", NotificationsPolicy.class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package org.joinmastodon.android.api.requests.notifications;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.ResultlessMastodonAPIRequest;
|
||||||
|
|
||||||
|
public class RespondToNotificationRequest extends ResultlessMastodonAPIRequest{
|
||||||
|
public RespondToNotificationRequest(String id, boolean allow){
|
||||||
|
super(HttpMethod.POST, "/notifications/requests/"+id+(allow ? "/accept" : "/dismiss"));
|
||||||
|
setRequestBody(new Object());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
package org.joinmastodon.android.api.requests.notifications;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||||
|
import org.joinmastodon.android.model.NotificationsPolicy;
|
||||||
|
|
||||||
|
public class SetNotificationsPolicy extends MastodonAPIRequest<NotificationsPolicy>{
|
||||||
|
public SetNotificationsPolicy(NotificationsPolicy policy){
|
||||||
|
super(HttpMethod.PUT, "/notifications/policy", NotificationsPolicy.class);
|
||||||
|
setRequestBody(policy);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package org.joinmastodon.android.events;
|
||||||
|
|
||||||
|
public class NotificationRequestRespondedEvent{
|
||||||
|
public final String accountID, requestID;
|
||||||
|
|
||||||
|
public NotificationRequestRespondedEvent(String accountID, String requestID){
|
||||||
|
this.accountID=accountID;
|
||||||
|
this.requestID=requestID;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,181 @@
|
||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.content.res.ColorStateList;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.notifications.GetNotifications;
|
||||||
|
import org.joinmastodon.android.api.requests.notifications.RespondToNotificationRequest;
|
||||||
|
import org.joinmastodon.android.events.NotificationRequestRespondedEvent;
|
||||||
|
import org.joinmastodon.android.model.Account;
|
||||||
|
import org.joinmastodon.android.model.Notification;
|
||||||
|
import org.joinmastodon.android.ui.Snackbar;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
|
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
|
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||||
|
|
||||||
|
public class AccountNotificationsListFragment extends BaseNotificationsListFragment{
|
||||||
|
private Account account;
|
||||||
|
private String requestID;
|
||||||
|
private TextView expandedTitle;
|
||||||
|
private boolean choiceMade, allowed;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
account=Parcels.unwrap(getArguments().getParcelable("targetAccount"));
|
||||||
|
requestID=getArguments().getString("requestID");
|
||||||
|
setTitleMarqueeEnabled(false);
|
||||||
|
loadData();
|
||||||
|
setTitle(getString(R.string.notifications_from_user, account.displayName));
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
if(!refreshing && endMark!=null)
|
||||||
|
endMark.setVisibility(View.GONE);
|
||||||
|
currentRequest=new GetNotifications(offset==0 ? null : maxID, count, EnumSet.allOf(Notification.Type.class), account.id)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(List<Notification> result){
|
||||||
|
onDataLoaded(result, !result.isEmpty());
|
||||||
|
maxID=result.isEmpty() ? null : result.get(result.size()-1).id;
|
||||||
|
endMark.setVisibility(result.isEmpty() ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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()>=list.getAdapter().getItemCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RecyclerView.Adapter getAdapter(){
|
||||||
|
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||||
|
|
||||||
|
expandedTitle=(TextView) LayoutInflater.from(getActivity()).inflate(R.layout.expanded_title_medium, list, false);
|
||||||
|
expandedTitle.setText(getTitle());
|
||||||
|
mergeAdapter.addAdapter(new SingleViewRecyclerAdapter(expandedTitle));
|
||||||
|
|
||||||
|
mergeAdapter.addAdapter(super.getAdapter());
|
||||||
|
return mergeAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
list.addOnScrollListener(new RecyclerView.OnScrollListener(){
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy){
|
||||||
|
if(recyclerView.getChildCount()==0)
|
||||||
|
return;
|
||||||
|
float fraction;
|
||||||
|
View topChild=recyclerView.getChildAt(0);
|
||||||
|
if(recyclerView.getChildAdapterPosition(topChild)>0){
|
||||||
|
fraction=1;
|
||||||
|
}else{
|
||||||
|
fraction=(-topChild.getTop())/(float)(topChild.getHeight()-topChild.getPaddingBottom());
|
||||||
|
}
|
||||||
|
expandedTitle.setAlpha(1f-fraction);
|
||||||
|
toolbarTitleView.setAlpha(fraction);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||||
|
inflater.inflate(R.menu.notification_request, menu);
|
||||||
|
MenuItem mute=menu.findItem(R.id.mute);
|
||||||
|
MenuItem allow=menu.findItem(R.id.allow);
|
||||||
|
if(choiceMade && allowed){
|
||||||
|
allow.setIcon(R.drawable.ic_check_wght700_24px);
|
||||||
|
tintMenuIcon(allow, R.attr.colorM3Primary);
|
||||||
|
}else{
|
||||||
|
tintMenuIcon(allow, R.attr.colorM3OnSurfaceVariant);
|
||||||
|
}
|
||||||
|
if(choiceMade && !allowed){
|
||||||
|
mute.setIcon(R.drawable.ic_volume_off_wght700_24px);
|
||||||
|
tintMenuIcon(mute, R.attr.colorM3Primary);
|
||||||
|
}else{
|
||||||
|
tintMenuIcon(mute, R.attr.colorM3OnSurfaceVariant);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
|
if(choiceMade)
|
||||||
|
return true;
|
||||||
|
allowed=item.getItemId()==R.id.allow;
|
||||||
|
new RespondToNotificationRequest(requestID, allowed)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Void result){
|
||||||
|
choiceMade=true;
|
||||||
|
invalidateOptionsMenu();
|
||||||
|
E.post(new NotificationRequestRespondedEvent(accountID, requestID));
|
||||||
|
new Snackbar.Builder(getActivity())
|
||||||
|
.setText(getString(allowed ? R.string.notifications_allowed : R.string.notifications_muted, account.displayName))
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(getActivity(), R.string.loading, false)
|
||||||
|
.exec(accountID);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
||||||
|
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
|
||||||
|
return StatusDisplayItem.buildItems(this, n.status, accountID, n, knownAccounts, StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
|
||||||
|
}
|
||||||
|
return super.buildDisplayItems(n);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean wantsToolbarMenuIconsTinted(){
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tintMenuIcon(MenuItem item, int color){
|
||||||
|
int tintColor=UiUtils.getThemeColor(getActivity(), color);
|
||||||
|
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.O){
|
||||||
|
Drawable icon=item.getIcon();
|
||||||
|
if(icon!=null && icon.getColorFilter()==null){
|
||||||
|
icon=icon.mutate();
|
||||||
|
icon.setTintList(ColorStateList.valueOf(tintColor));
|
||||||
|
item.setIcon(icon);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
item.setIconTintList(ColorStateList.valueOf(tintColor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -140,11 +140,6 @@ public class AccountTimelineFragment extends StatusListFragment{
|
||||||
return mergeAdapter;
|
return mergeAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected int getMainAdapterOffset(){
|
|
||||||
return super.getMainAdapterOffset()+1;
|
|
||||||
}
|
|
||||||
|
|
||||||
private FilterChipView getViewForFilter(GetAccountStatuses.Filter filter){
|
private FilterChipView getViewForFilter(GetAccountStatuses.Filter filter){
|
||||||
return switch(filter){
|
return switch(filter){
|
||||||
case DEFAULT -> defaultFilter;
|
case DEFAULT -> defaultFilter;
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.model.Notification;
|
||||||
|
import org.joinmastodon.android.model.Status;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
|
||||||
|
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||||
|
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import me.grishka.appkit.Nav;
|
||||||
|
|
||||||
|
public abstract class BaseNotificationsListFragment extends BaseStatusListFragment<Notification>{
|
||||||
|
protected String maxID;
|
||||||
|
protected View endMark;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
||||||
|
NotificationHeaderStatusDisplayItem titleItem;
|
||||||
|
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
|
||||||
|
titleItem=null;
|
||||||
|
}else{
|
||||||
|
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
|
||||||
|
if(n.status!=null){
|
||||||
|
n.status.card=null;
|
||||||
|
n.status.spoilerText=null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(n.status!=null){
|
||||||
|
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)
|
||||||
|
items.add(0, titleItem);
|
||||||
|
return items;
|
||||||
|
}else if(titleItem!=null){
|
||||||
|
return Collections.singletonList(titleItem);
|
||||||
|
}else{
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void addAccountToKnown(Notification s){
|
||||||
|
if(!knownAccounts.containsKey(s.account.id))
|
||||||
|
knownAccounts.put(s.account.id, s.account);
|
||||||
|
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
|
||||||
|
knownAccounts.put(s.status.account.id, s.status.account);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onItemClick(String id){
|
||||||
|
Notification n=getNotificationByID(id);
|
||||||
|
if(n.status!=null){
|
||||||
|
Status status=n.status;
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("status", Parcels.wrap(status.clone()));
|
||||||
|
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
||||||
|
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
||||||
|
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||||
|
}else{
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("profileAccount", Parcels.wrap(n.account));
|
||||||
|
Nav.go(getActivity(), ProfileFragment.class, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Notification getNotificationByID(String id){
|
||||||
|
for(Notification n : data){
|
||||||
|
if(n.id.equals(id))
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void removeNotification(Notification n){
|
||||||
|
data.remove(n);
|
||||||
|
preloadedData.remove(n);
|
||||||
|
int index=-1;
|
||||||
|
for(int i=0; i<displayItems.size(); i++){
|
||||||
|
if(n.id.equals(displayItems.get(i).parentID)){
|
||||||
|
index=i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(index==-1)
|
||||||
|
return;
|
||||||
|
int lastIndex;
|
||||||
|
for(lastIndex=index; lastIndex<displayItems.size(); lastIndex++){
|
||||||
|
if(!displayItems.get(lastIndex).parentID.equals(n.id))
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
displayItems.subList(index, lastIndex).clear();
|
||||||
|
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||||
|
}
|
||||||
|
}
|
|
@ -325,7 +325,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||||
toolbar.setNavigationContentDescription(R.string.back);
|
toolbar.setNavigationContentDescription(R.string.back);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int getMainAdapterOffset(){
|
public int getMainAdapterOffset(){
|
||||||
if(list.getAdapter() instanceof MergeRecyclerAdapter mergeAdapter){
|
if(list.getAdapter() instanceof MergeRecyclerAdapter mergeAdapter){
|
||||||
return mergeAdapter.getPositionForAdapter(adapter);
|
return mergeAdapter.getPositionForAdapter(adapter);
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,6 @@ import me.grishka.appkit.Nav;
|
||||||
import me.grishka.appkit.api.Callback;
|
import me.grishka.appkit.api.Callback;
|
||||||
import me.grishka.appkit.api.ErrorResponse;
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
import me.grishka.appkit.utils.CubicBezierInterpolator;
|
|
||||||
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||||
import me.grishka.appkit.utils.V;
|
import me.grishka.appkit.utils.V;
|
||||||
|
@ -155,7 +154,7 @@ public class HashtagTimelineFragment extends StatusListFragment{
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getMainAdapterOffset(){
|
public int getMainAdapterOffset(){
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,250 @@
|
||||||
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.squareup.otto.Subscribe;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.E;
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.api.requests.notifications.GetNotificationRequests;
|
||||||
|
import org.joinmastodon.android.api.requests.notifications.RespondToNotificationRequest;
|
||||||
|
import org.joinmastodon.android.events.NotificationRequestRespondedEvent;
|
||||||
|
import org.joinmastodon.android.model.HeaderPaginationList;
|
||||||
|
import org.joinmastodon.android.model.NotificationRequest;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.AccountViewModel;
|
||||||
|
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||||
|
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||||
|
import org.joinmastodon.android.ui.OutlineProviders;
|
||||||
|
import org.joinmastodon.android.ui.Snackbar;
|
||||||
|
import org.parceler.Parcels;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import me.grishka.appkit.Nav;
|
||||||
|
import me.grishka.appkit.api.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter;
|
||||||
|
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||||
|
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||||
|
import me.grishka.appkit.utils.BindableViewHolder;
|
||||||
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
|
|
||||||
|
public class NotificationRequestsFragment extends MastodonRecyclerFragment<NotificationRequest>{
|
||||||
|
private String accountID;
|
||||||
|
private String maxID;
|
||||||
|
private HashMap<String, AccountViewModel> accountViewModels=new HashMap<>();
|
||||||
|
private View endMark;
|
||||||
|
private NotificationRequestsAdapter adapter;
|
||||||
|
|
||||||
|
public NotificationRequestsFragment(){
|
||||||
|
super(50);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
accountID=getArguments().getString("account");
|
||||||
|
setTitle(R.string.filtered_notifications);
|
||||||
|
loadData();
|
||||||
|
E.register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy(){
|
||||||
|
E.unregister(this);
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void doLoadData(int offset, int count){
|
||||||
|
if(!refreshing && endMark!=null)
|
||||||
|
endMark.setVisibility(View.GONE);
|
||||||
|
currentRequest=new GetNotificationRequests(offset==0 ? null : maxID)
|
||||||
|
.setCallback(new SimpleCallback<>(this){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(HeaderPaginationList<NotificationRequest> result){
|
||||||
|
if(data.isEmpty() || refreshing)
|
||||||
|
accountViewModels.clear();
|
||||||
|
maxID=result.getNextPageMaxID();
|
||||||
|
for(NotificationRequest req:result){
|
||||||
|
accountViewModels.put(req.account.id, new AccountViewModel(req.account, accountID, false));
|
||||||
|
}
|
||||||
|
onDataLoaded(result, !TextUtils.isEmpty(maxID));
|
||||||
|
endMark.setVisibility(TextUtils.isEmpty(maxID) ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RecyclerView.Adapter<?> getAdapter(){
|
||||||
|
return adapter=new NotificationRequestsAdapter();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
list.setItemAnimator(new BetterItemAnimator());
|
||||||
|
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 1, 0, 0, vh->vh instanceof NotificationRequestViewHolder).setDrawBelowLastItem(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe
|
||||||
|
public void onNotificationRequestResponded(NotificationRequestRespondedEvent ev){
|
||||||
|
if(adapter==null || !ev.accountID.equals(accountID))
|
||||||
|
return;
|
||||||
|
for(int i=0;i<data.size();i++){
|
||||||
|
if(data.get(i).id.equals(ev.requestID)){
|
||||||
|
data.remove(i);
|
||||||
|
adapter.notifyItemRemoved(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(NotificationRequest nr:preloadedData){
|
||||||
|
if(nr.id.equals(ev.requestID)){
|
||||||
|
preloadedData.remove(nr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NotificationRequestsAdapter extends UsableRecyclerView.Adapter<NotificationRequestViewHolder> implements ImageLoaderRecyclerAdapter{
|
||||||
|
|
||||||
|
public NotificationRequestsAdapter(){
|
||||||
|
super(imgLoader);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public NotificationRequestViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||||
|
return new NotificationRequestViewHolder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount(){
|
||||||
|
return data.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(NotificationRequestViewHolder holder, int position){
|
||||||
|
holder.bind(data.get(position));
|
||||||
|
super.onBindViewHolder(holder, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getImageCountForItem(int position){
|
||||||
|
return Objects.requireNonNull(accountViewModels.get(data.get(position).account.id)).emojiHelper.getImageCount()+1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ImageLoaderRequest getImageRequest(int position, int image){
|
||||||
|
AccountViewModel model=Objects.requireNonNull(accountViewModels.get(data.get(position).account.id));
|
||||||
|
return switch(image){
|
||||||
|
case 0 -> model.avaRequest;
|
||||||
|
default -> model.emojiHelper.getImageRequest(image-1);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class NotificationRequestViewHolder extends BindableViewHolder<NotificationRequest> implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{
|
||||||
|
private final TextView name, username, badge;
|
||||||
|
private final ImageView ava;
|
||||||
|
private final ImageButton allow, mute;
|
||||||
|
|
||||||
|
public NotificationRequestViewHolder(){
|
||||||
|
super(getActivity(), R.layout.item_notification_request, list);
|
||||||
|
name=findViewById(R.id.name);
|
||||||
|
username=findViewById(R.id.username);
|
||||||
|
badge=findViewById(R.id.badge);
|
||||||
|
ava=findViewById(R.id.ava);
|
||||||
|
allow=findViewById(R.id.btn_allow);
|
||||||
|
mute=findViewById(R.id.btn_mute);
|
||||||
|
ava.setOutlineProvider(OutlineProviders.roundedRect(8));
|
||||||
|
ava.setClipToOutline(true);
|
||||||
|
allow.setOnClickListener(this::onAllowClick);
|
||||||
|
mute.setOnClickListener(this::onMuteClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("DefaultLocale")
|
||||||
|
@Override
|
||||||
|
public void onBind(NotificationRequest item){
|
||||||
|
AccountViewModel model=Objects.requireNonNull(accountViewModels.get(item.account.id));
|
||||||
|
name.setText(model.parsedName);
|
||||||
|
username.setText(item.account.getDisplayUsername());
|
||||||
|
badge.setText(item.notificationsCount>99 ? String.format("%d+", 99) : String.format("%d", item.notificationsCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setImage(int index, Drawable image){
|
||||||
|
if(index==0){
|
||||||
|
if(image==null)
|
||||||
|
ava.setImageResource(R.drawable.image_placeholder);
|
||||||
|
else
|
||||||
|
ava.setImageDrawable(image);
|
||||||
|
}else{
|
||||||
|
AccountViewModel model=Objects.requireNonNull(accountViewModels.get(item.account.id));
|
||||||
|
model.emojiHelper.setImageDrawable(index-1, image);
|
||||||
|
name.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClick(){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
args.putParcelable("targetAccount", Parcels.wrap(item.account));
|
||||||
|
args.putString("requestID", item.id);
|
||||||
|
Nav.go(getActivity(), AccountNotificationsListFragment.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onAllowClick(View v){
|
||||||
|
acceptOrDecline(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onMuteClick(View v){
|
||||||
|
acceptOrDecline(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acceptOrDecline(boolean accept){
|
||||||
|
new RespondToNotificationRequest(item.id, accept)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Void result){
|
||||||
|
int pos=data.indexOf(item);
|
||||||
|
data.remove(pos);
|
||||||
|
adapter.notifyItemRemoved(pos);
|
||||||
|
new Snackbar.Builder(getActivity())
|
||||||
|
.setText(getString(accept ? R.string.notifications_allowed : R.string.notifications_muted, item.account.displayName))
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse error){
|
||||||
|
error.showToast(getActivity());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.wrapProgress(getActivity(), R.string.loading, false)
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,57 +1,70 @@
|
||||||
package org.joinmastodon.android.fragments;
|
package org.joinmastodon.android.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
|
import android.app.AlertDialog;
|
||||||
|
import android.app.Dialog;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.widget.Button;
|
||||||
|
|
||||||
import com.squareup.otto.Subscribe;
|
import com.squareup.otto.Subscribe;
|
||||||
|
|
||||||
import org.joinmastodon.android.E;
|
import org.joinmastodon.android.E;
|
||||||
import org.joinmastodon.android.R;
|
import org.joinmastodon.android.R;
|
||||||
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
import org.joinmastodon.android.api.requests.markers.SaveMarkers;
|
||||||
|
import org.joinmastodon.android.api.requests.notifications.GetNotificationsPolicy;
|
||||||
|
import org.joinmastodon.android.api.requests.notifications.SetNotificationsPolicy;
|
||||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||||
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
import org.joinmastodon.android.events.RemoveAccountPostsEvent;
|
||||||
import org.joinmastodon.android.model.Notification;
|
import org.joinmastodon.android.model.Notification;
|
||||||
|
import org.joinmastodon.android.model.NotificationsPolicy;
|
||||||
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.model.viewmodel.CheckableListItem;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||||
|
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||||
import org.joinmastodon.android.ui.OutlineProviders;
|
import org.joinmastodon.android.ui.OutlineProviders;
|
||||||
import org.joinmastodon.android.ui.displayitems.NotificationHeaderStatusDisplayItem;
|
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||||
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.utils.UiUtils;
|
||||||
|
import org.joinmastodon.android.ui.viewcontrollers.GenericListItemsViewController;
|
||||||
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
|
import org.joinmastodon.android.ui.views.NestedRecyclerScrollView;
|
||||||
import org.joinmastodon.android.utils.ObjectIdComparator;
|
import org.joinmastodon.android.utils.ObjectIdComparator;
|
||||||
import org.parceler.Parcels;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
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.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.Callback;
|
||||||
|
import me.grishka.appkit.api.ErrorResponse;
|
||||||
import me.grishka.appkit.api.SimpleCallback;
|
import me.grishka.appkit.api.SimpleCallback;
|
||||||
|
import me.grishka.appkit.utils.MergeRecyclerAdapter;
|
||||||
|
|
||||||
public class NotificationsListFragment extends BaseStatusListFragment<Notification>{
|
public class NotificationsListFragment extends BaseNotificationsListFragment{
|
||||||
private boolean onlyMentions;
|
private boolean onlyMentions;
|
||||||
private String maxID;
|
|
||||||
private View tabBar;
|
private View tabBar;
|
||||||
private View mentionsTab, allTab;
|
private View mentionsTab, allTab;
|
||||||
private View endMark;
|
|
||||||
private String unreadMarker, realUnreadMarker;
|
private String unreadMarker, realUnreadMarker;
|
||||||
private MenuItem markAllReadItem;
|
private MenuItem markAllReadItem;
|
||||||
private boolean reloadingFromCache;
|
private boolean reloadingFromCache;
|
||||||
|
private ListItem<Void> requestsItem=new ListItem<>(R.string.filtered_notifications, 0, R.drawable.ic_inventory_2_24px, i->openNotificationRequests());
|
||||||
|
private ArrayList<ListItem<Void>> requestsItems=new ArrayList<>();
|
||||||
|
private GenericListItemsAdapter<Void> requestsRowAdapter=new GenericListItemsAdapter<>(requestsItems);
|
||||||
|
private NotificationsPolicy lastPolicy;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState){
|
public void onCreate(Bundle savedInstanceState){
|
||||||
|
@ -74,43 +87,12 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||||
setTitle(R.string.notifications);
|
setTitle(R.string.notifications);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected List<StatusDisplayItem> buildDisplayItems(Notification n){
|
|
||||||
NotificationHeaderStatusDisplayItem titleItem;
|
|
||||||
if(n.type==Notification.Type.MENTION || n.type==Notification.Type.STATUS){
|
|
||||||
titleItem=null;
|
|
||||||
}else{
|
|
||||||
titleItem=new NotificationHeaderStatusDisplayItem(n.id, this, n, accountID);
|
|
||||||
if(n.status!=null){
|
|
||||||
n.status.card=null;
|
|
||||||
n.status.spoilerText=null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(n.status!=null){
|
|
||||||
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)
|
|
||||||
items.add(0, titleItem);
|
|
||||||
return items;
|
|
||||||
}else if(titleItem!=null){
|
|
||||||
return Collections.singletonList(titleItem);
|
|
||||||
}else{
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void addAccountToKnown(Notification s){
|
|
||||||
if(!knownAccounts.containsKey(s.account.id))
|
|
||||||
knownAccounts.put(s.account.id, s.account);
|
|
||||||
if(s.status!=null && !knownAccounts.containsKey(s.status.account.id))
|
|
||||||
knownAccounts.put(s.status.account.id, s.status.account);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doLoadData(int offset, int count){
|
protected void doLoadData(int offset, int count){
|
||||||
if(!refreshing && !reloadingFromCache)
|
if(!refreshing && !reloadingFromCache)
|
||||||
endMark.setVisibility(View.GONE);
|
endMark.setVisibility(View.GONE);
|
||||||
|
if(offset==0)
|
||||||
|
reloadPolicy();
|
||||||
AccountSessionManager.getInstance()
|
AccountSessionManager.getInstance()
|
||||||
.getAccount(accountID).getCacheController()
|
.getAccount(accountID).getCacheController()
|
||||||
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
|
.getNotifications(offset>0 ? maxID : null, count, onlyMentions, refreshing && !reloadingFromCache, new SimpleCallback<>(this){
|
||||||
|
@ -142,30 +124,10 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||||
resetUnreadBackground();
|
resetUnreadBackground();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onItemClick(String id){
|
|
||||||
Notification n=getNotificationByID(id);
|
|
||||||
if(n.status!=null){
|
|
||||||
Status status=n.status;
|
|
||||||
Bundle args=new Bundle();
|
|
||||||
args.putString("account", accountID);
|
|
||||||
args.putParcelable("status", Parcels.wrap(status.clone()));
|
|
||||||
if(status.inReplyToAccountId!=null && knownAccounts.containsKey(status.inReplyToAccountId))
|
|
||||||
args.putParcelable("inReplyToAccount", Parcels.wrap(knownAccounts.get(status.inReplyToAccountId)));
|
|
||||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
|
||||||
}else{
|
|
||||||
Bundle args=new Bundle();
|
|
||||||
args.putString("account", accountID);
|
|
||||||
args.putParcelable("profileAccount", Parcels.wrap(n.account));
|
|
||||||
Nav.go(getActivity(), ProfileFragment.class, args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||||
tabBar=view.findViewById(R.id.tabbar);
|
tabBar=view.findViewById(R.id.tabbar);
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
|
||||||
|
|
||||||
View tabBarItself=view.findViewById(R.id.tabbar_inner);
|
View tabBarItself=view.findViewById(R.id.tabbar_inner);
|
||||||
tabBarItself.setOutlineProvider(OutlineProviders.roundedRect(20));
|
tabBarItself.setOutlineProvider(OutlineProviders.roundedRect(20));
|
||||||
|
@ -215,14 +177,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||||
return views;
|
return views;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Notification getNotificationByID(String id){
|
|
||||||
for(Notification n:data){
|
|
||||||
if(n.id.equals(id))
|
|
||||||
return n;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Subscribe
|
@Subscribe
|
||||||
public void onPollUpdated(PollUpdatedEvent ev){
|
public void onPollUpdated(PollUpdatedEvent ev){
|
||||||
if(!ev.accountID.equals(accountID))
|
if(!ev.accountID.equals(accountID))
|
||||||
|
@ -249,25 +203,9 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void removeNotification(Notification n){
|
@Override
|
||||||
data.remove(n);
|
protected boolean needDividerForExtraItem(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder){
|
||||||
preloadedData.remove(n);
|
return super.needDividerForExtraItem(child, bottomSibling, holder, siblingHolder) || (siblingHolder!=null && siblingHolder.getAbsoluteAdapterPosition()>=adapter.getItemCount()) || holder.getAbsoluteAdapterPosition()<requestsItems.size();
|
||||||
int index=-1;
|
|
||||||
for(int i=0;i<displayItems.size();i++){
|
|
||||||
if(n.id.equals(displayItems.get(i).parentID)){
|
|
||||||
index=i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if(index==-1)
|
|
||||||
return;
|
|
||||||
int lastIndex;
|
|
||||||
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
|
|
||||||
if(!displayItems.get(lastIndex).parentID.equals(n.id))
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
displayItems.subList(index, lastIndex).clear();
|
|
||||||
adapter.notifyItemRangeRemoved(index, lastIndex-index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onTabClick(View v){
|
private void onTabClick(View v){
|
||||||
|
@ -285,34 +223,34 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||||
AccountSessionManager.get(accountID).setNotificationsMentionsOnly(onlyMentions);
|
AccountSessionManager.get(accountID).setNotificationsMentionsOnly(onlyMentions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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
|
@Override
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||||
inflater.inflate(R.menu.notifications, menu);
|
inflater.inflate(R.menu.notifications, menu);
|
||||||
markAllReadItem=menu.findItem(R.id.mark_all_read);
|
markAllReadItem=menu.findItem(R.id.mark_all_read);
|
||||||
|
MenuItem filters=menu.findItem(R.id.filters);
|
||||||
|
filters.setVisible(lastPolicy!=null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item){
|
public boolean onOptionsItemSelected(MenuItem item){
|
||||||
if(item.getItemId()==R.id.mark_all_read){
|
int id=item.getItemId();
|
||||||
|
if(id==R.id.mark_all_read){
|
||||||
markAsRead();
|
markAsRead();
|
||||||
resetUnreadBackground();
|
resetUnreadBackground();
|
||||||
|
}else if(id==R.id.filters){
|
||||||
|
showFiltersAlert();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected RecyclerView.Adapter getAdapter(){
|
||||||
|
MergeRecyclerAdapter mergeAdapter=new MergeRecyclerAdapter();
|
||||||
|
mergeAdapter.addAdapter(requestsRowAdapter);
|
||||||
|
mergeAdapter.addAdapter(super.getAdapter());
|
||||||
|
return mergeAdapter;
|
||||||
|
}
|
||||||
|
|
||||||
private void markAsRead(){
|
private void markAsRead(){
|
||||||
if(data.isEmpty())
|
if(data.isEmpty())
|
||||||
return;
|
return;
|
||||||
|
@ -366,4 +304,93 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updatePolicy(NotificationsPolicy policy){
|
||||||
|
int count=policy.summary==null ? 0 : policy.summary.pendingRequestsCount;
|
||||||
|
boolean isShown=!requestsItems.isEmpty();
|
||||||
|
boolean needShow=count>0;
|
||||||
|
if(isShown && !needShow){
|
||||||
|
requestsItems.clear();
|
||||||
|
requestsRowAdapter.notifyItemRemoved(0);
|
||||||
|
}else if(!isShown && needShow){
|
||||||
|
requestsItem.subtitle=getResources().getQuantityString(R.plurals.x_people_you_may_know, count, count);
|
||||||
|
requestsItems.add(requestsItem);
|
||||||
|
requestsRowAdapter.notifyItemInserted(0);
|
||||||
|
}else if(isShown){
|
||||||
|
requestsItem.subtitle=getResources().getQuantityString(R.plurals.x_people_you_may_know, count, count);
|
||||||
|
requestsRowAdapter.notifyItemChanged(0);
|
||||||
|
}
|
||||||
|
lastPolicy=policy;
|
||||||
|
invalidateOptionsMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void reloadPolicy(){
|
||||||
|
new GetNotificationsPolicy()
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(NotificationsPolicy policy){
|
||||||
|
updatePolicy(policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse errorResponse){
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showFiltersAlert(){
|
||||||
|
GenericListItemsViewController<Void> controller=new GenericListItemsViewController<>(getActivity());
|
||||||
|
Consumer<CheckableListItem<Void>> toggler=item->{
|
||||||
|
item.toggle();
|
||||||
|
controller.rebindItem(item);
|
||||||
|
};
|
||||||
|
CheckableListItem<Void> followingItem, followersItem, newAccountsItem, mentionsItem;
|
||||||
|
List<ListItem<Void>> items=List.of(
|
||||||
|
followingItem=new CheckableListItem<>(R.string.notification_filter_following, R.string.notification_filter_following_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterNotFollowing, toggler, true),
|
||||||
|
followersItem=new CheckableListItem<>(R.string.notification_filter_followers, R.string.notification_filter_followers_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterNotFollowers, toggler, true),
|
||||||
|
newAccountsItem=new CheckableListItem<>(R.string.notification_filter_new_accounts, R.string.notification_filter_new_accounts_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterNewAccounts, toggler, true),
|
||||||
|
mentionsItem=new CheckableListItem<>(R.string.notification_filter_mentions, R.string.notification_filter_mentions_explanation, CheckableListItem.Style.CHECKBOX, lastPolicy.filterPrivateMentions, toggler, true)
|
||||||
|
);
|
||||||
|
controller.setItems(items);
|
||||||
|
AlertDialog dlg=new M3AlertDialogBuilder(getActivity())
|
||||||
|
.setTitle(R.string.filter_notifications)
|
||||||
|
.setView(controller.getView())
|
||||||
|
.setPositiveButton(R.string.save, null)
|
||||||
|
.show();
|
||||||
|
Button btn=dlg.getButton(Dialog.BUTTON_POSITIVE);
|
||||||
|
btn.setOnClickListener(v->{
|
||||||
|
UiUtils.showProgressForAlertButton(btn, true);
|
||||||
|
NotificationsPolicy newPolicy=new NotificationsPolicy();
|
||||||
|
newPolicy.filterNotFollowing=followingItem.checked;
|
||||||
|
newPolicy.filterNotFollowers=followersItem.checked;
|
||||||
|
newPolicy.filterNewAccounts=newAccountsItem.checked;
|
||||||
|
newPolicy.filterPrivateMentions=mentionsItem.checked;
|
||||||
|
new SetNotificationsPolicy(newPolicy)
|
||||||
|
.setCallback(new Callback<>(){
|
||||||
|
@Override
|
||||||
|
public void onSuccess(NotificationsPolicy policy){
|
||||||
|
updatePolicy(policy);
|
||||||
|
dlg.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(ErrorResponse errorResponse){
|
||||||
|
Activity activity=getActivity();
|
||||||
|
if(activity==null)
|
||||||
|
return;
|
||||||
|
UiUtils.showProgressForAlertButton(btn, false);
|
||||||
|
errorResponse.showToast(activity);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.exec(accountID);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openNotificationRequests(){
|
||||||
|
Bundle args=new Bundle();
|
||||||
|
args.putString("account", accountID);
|
||||||
|
Nav.go(getActivity(), NotificationRequestsFragment.class, args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,7 +134,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected int getMainAdapterOffset(){
|
public int getMainAdapterOffset(){
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
package org.joinmastodon.android.model;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.api.ObjectValidationException;
|
||||||
|
import org.joinmastodon.android.api.RequiredField;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
public class NotificationRequest extends BaseModel{
|
||||||
|
@RequiredField
|
||||||
|
public String id;
|
||||||
|
@RequiredField
|
||||||
|
public Instant createdAt;
|
||||||
|
@RequiredField
|
||||||
|
public Instant updatedAt;
|
||||||
|
public int notificationsCount;
|
||||||
|
@RequiredField
|
||||||
|
public Account account;
|
||||||
|
public Status lastStatus;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void postprocess() throws ObjectValidationException{
|
||||||
|
super.postprocess();
|
||||||
|
account.postprocess();
|
||||||
|
if(lastStatus!=null)
|
||||||
|
lastStatus.postprocess();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package org.joinmastodon.android.model;
|
||||||
|
|
||||||
|
public class NotificationsPolicy extends BaseModel{
|
||||||
|
public boolean filterNewAccounts;
|
||||||
|
public boolean filterNotFollowers;
|
||||||
|
public boolean filterNotFollowing;
|
||||||
|
public boolean filterPrivateMentions;
|
||||||
|
public Summary summary;
|
||||||
|
|
||||||
|
public static class Summary{
|
||||||
|
public int pendingNotificationsCount;
|
||||||
|
public int pendingRequestsCount;
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,6 +25,10 @@ public class AccountViewModel{
|
||||||
public final String verifiedLink;
|
public final String verifiedLink;
|
||||||
|
|
||||||
public AccountViewModel(Account account, String accountID){
|
public AccountViewModel(Account account, String accountID){
|
||||||
|
this(account, accountID, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public AccountViewModel(Account account, String accountID, boolean needBio){
|
||||||
this.account=account;
|
this.account=account;
|
||||||
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
|
avaRequest=new UrlImageLoaderRequest(GlobalUserPreferences.playGifs ? account.avatar : account.avatarStatic, V.dp(50), V.dp(50));
|
||||||
emojiHelper=new CustomEmojiHelper();
|
emojiHelper=new CustomEmojiHelper();
|
||||||
|
@ -32,9 +36,13 @@ public class AccountViewModel{
|
||||||
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis);
|
||||||
else
|
else
|
||||||
parsedName=account.displayName;
|
parsedName=account.displayName;
|
||||||
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
|
|
||||||
SpannableStringBuilder ssb=new SpannableStringBuilder(parsedName);
|
SpannableStringBuilder ssb=new SpannableStringBuilder(parsedName);
|
||||||
|
if(needBio){
|
||||||
|
parsedBio=HtmlParser.parse(account.note, account.emojis, Collections.emptyList(), Collections.emptyList(), accountID, account);
|
||||||
ssb.append(parsedBio);
|
ssb.append(parsedBio);
|
||||||
|
}else{
|
||||||
|
parsedBio=null;
|
||||||
|
}
|
||||||
emojiHelper.setText(ssb);
|
emojiHelper.setText(ssb);
|
||||||
String verifiedLink=null;
|
String verifiedLink=null;
|
||||||
for(AccountField fld:account.fields){
|
for(AccountField fld:account.fields){
|
||||||
|
|
|
@ -35,8 +35,9 @@ public class DividerItemDecoration extends RecyclerView.ItemDecoration{
|
||||||
this.drawDividerPredicate=drawDividerPredicate;
|
this.drawDividerPredicate=drawDividerPredicate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDrawBelowLastItem(boolean drawBelowLastItem){
|
public DividerItemDecoration setDrawBelowLastItem(boolean drawBelowLastItem){
|
||||||
this.drawBelowLastItem=drawBelowLastItem;
|
this.drawBelowLastItem=drawBelowLastItem;
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -151,10 +151,12 @@ public abstract class StatusDisplayItem{
|
||||||
if(!imageAttachments.isEmpty()){
|
if(!imageAttachments.isEmpty()){
|
||||||
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
|
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
|
||||||
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
|
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
|
||||||
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0)
|
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0){
|
||||||
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
|
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
|
||||||
else if(statusForContent.sensitive && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
|
mediaGrid.sensitiveRevealed=false;
|
||||||
|
}else if(statusForContent.sensitive && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia){
|
||||||
mediaGrid.sensitiveRevealed=true;
|
mediaGrid.sensitiveRevealed=true;
|
||||||
|
}
|
||||||
contentItems.add(mediaGrid);
|
contentItems.add(mediaGrid);
|
||||||
}
|
}
|
||||||
for(Attachment att:statusForContent.mediaAttachments){
|
for(Attachment att:statusForContent.mediaAttachments){
|
||||||
|
|
|
@ -37,7 +37,7 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
|
||||||
for(int i=0; i<parent.getChildCount(); i++){
|
for(int i=0; i<parent.getChildCount(); i++){
|
||||||
View child=parent.getChildAt(i);
|
View child=parent.getChildAt(i);
|
||||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
RecyclerView.ViewHolder holder=parent.getChildViewHolder(child);
|
||||||
pos=holder.getAbsoluteAdapterPosition();
|
pos=holder.getAbsoluteAdapterPosition()-listFragment.getMainAdapterOffset();
|
||||||
boolean inset=(holder instanceof StatusDisplayItem.Holder<?> sdi) && sdi.getItem().inset;
|
boolean inset=(holder instanceof StatusDisplayItem.Holder<?> sdi) && sdi.getItem().inset;
|
||||||
if(inset){
|
if(inset){
|
||||||
if(rect.isEmpty()){
|
if(rect.isEmpty()){
|
||||||
|
@ -82,7 +82,7 @@ public class InsetStatusItemDecoration extends RecyclerView.ItemDecoration{
|
||||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
||||||
if(holder instanceof StatusDisplayItem.Holder<?> sdi){
|
if(holder instanceof StatusDisplayItem.Holder<?> sdi){
|
||||||
boolean inset=sdi.getItem().inset;
|
boolean inset=sdi.getItem().inset;
|
||||||
int pos=holder.getAbsoluteAdapterPosition();
|
int pos=holder.getAbsoluteAdapterPosition()-listFragment.getMainAdapterOffset();
|
||||||
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;
|
||||||
|
|
|
@ -0,0 +1,60 @@
|
||||||
|
package org.joinmastodon.android.ui.viewcontrollers;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import org.joinmastodon.android.R;
|
||||||
|
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||||
|
import org.joinmastodon.android.ui.BetterItemAnimator;
|
||||||
|
import org.joinmastodon.android.ui.DividerItemDecoration;
|
||||||
|
import org.joinmastodon.android.ui.adapters.GenericListItemsAdapter;
|
||||||
|
import org.joinmastodon.android.ui.viewholders.CheckableListItemViewHolder;
|
||||||
|
import org.joinmastodon.android.ui.viewholders.ListItemViewHolder;
|
||||||
|
import org.joinmastodon.android.ui.viewholders.SimpleListItemViewHolder;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import me.grishka.appkit.views.UsableRecyclerView;
|
||||||
|
|
||||||
|
public class GenericListItemsViewController<T>{
|
||||||
|
private UsableRecyclerView list;
|
||||||
|
private List<ListItem<T>> items;
|
||||||
|
private GenericListItemsAdapter<T> adapter;
|
||||||
|
private Context context;
|
||||||
|
|
||||||
|
public GenericListItemsViewController(Context context, List<ListItem<T>> items){
|
||||||
|
this.context=context;
|
||||||
|
setItems(items);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GenericListItemsViewController(Context context){
|
||||||
|
this.context=context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setItems(List<ListItem<T>> items){
|
||||||
|
if(this.items!=null)
|
||||||
|
throw new IllegalStateException("items already set");
|
||||||
|
this.items=items;
|
||||||
|
adapter=new GenericListItemsAdapter<>(items);
|
||||||
|
list=new UsableRecyclerView(context);
|
||||||
|
list.setLayoutManager(new LinearLayoutManager(context));
|
||||||
|
list.setAdapter(adapter);
|
||||||
|
list.addItemDecoration(new DividerItemDecoration(context, R.attr.colorM3OutlineVariant, 1, 16, 16, vh->(vh instanceof SimpleListItemViewHolder ivh && ivh.getItem().dividerAfter) || (vh instanceof CheckableListItemViewHolder cvh && cvh.getItem().dividerAfter)));
|
||||||
|
list.setItemAnimator(new BetterItemAnimator());
|
||||||
|
}
|
||||||
|
|
||||||
|
public GenericListItemsAdapter<T> getAdapter(){
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public View getView(){
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void rebindItem(ListItem<?> item){
|
||||||
|
if(list.findViewHolderForAdapterPosition(items.indexOf(item)) instanceof ListItemViewHolder<?> holder){
|
||||||
|
holder.rebind();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<item android:color="?colorM3OnSurfaceVariant" android:state_enabled="true"/>
|
<item android:color="?colorM3OnSurfaceVariant" android:state_enabled="true"/>
|
||||||
<item android:color="?colorM3OnSurfaceVariant" android:alpha="0.38"/>
|
<item android:color="?colorM3OnSurface" android:alpha="0.38"/>
|
||||||
</selector>
|
</selector>
|
6
mastodon/src/main/res/drawable/bg_ava_badge.xml
Normal file
6
mastodon/src/main/res/drawable/bg_ava_badge.xml
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="?colorM3Primary"/>
|
||||||
|
<corners android:radius="100dp"/>
|
||||||
|
<stroke android:color="?colorM3Background" android:width="1dp"/>
|
||||||
|
</shape>
|
9
mastodon/src/main/res/drawable/ic_check_wght700_24px.xml
Normal file
9
mastodon/src/main/res/drawable/ic_check_wght700_24px.xml
Normal file
|
@ -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="M9.55,18.8 L3.05,12.3 5.3,10.05 9.55,14.3 18.7,5.15 20.95,7.4Z"/>
|
||||||
|
</vector>
|
9
mastodon/src/main/res/drawable/ic_inventory_2_24px.xml
Normal file
9
mastodon/src/main/res/drawable/ic_inventory_2_24px.xml
Normal file
|
@ -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="M3,20V8.7Q2.575,8.425 2.288,8Q2,7.575 2,7V4Q2,3.175 2.588,2.587Q3.175,2 4,2H20Q20.825,2 21.413,2.587Q22,3.175 22,4V7Q22,7.575 21.712,8Q21.425,8.425 21,8.7V20Q21,20.825 20.413,21.413Q19.825,22 19,22H5Q4.175,22 3.587,21.413Q3,20.825 3,20ZM5,9V20Q5,20 5,20Q5,20 5,20H19Q19,20 19,20Q19,20 19,20V9ZM20,7Q20,7 20,7Q20,7 20,7V4Q20,4 20,4Q20,4 20,4H4Q4,4 4,4Q4,4 4,4V7Q4,7 4,7Q4,7 4,7ZM9,14H15V12H9ZM5,20Q5,20 5,20Q5,20 5,20V9V20Q5,20 5,20Q5,20 5,20Z"/>
|
||||||
|
</vector>
|
9
mastodon/src/main/res/drawable/ic_tune_24px.xml
Normal file
9
mastodon/src/main/res/drawable/ic_tune_24px.xml
Normal file
|
@ -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="M3,19V17H9V19ZM3,7V5H13V7ZM11,21V15H13V17H21V19H13V21ZM7,15V13H3V11H7V9H9V15ZM11,13V11H21V13ZM15,9V3H17V5H21V7H17V9Z"/>
|
||||||
|
</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="M20.05,23.4 L16.975,20.275Q16.45,20.575 15.863,20.8Q15.275,21.025 14.625,21.15V18.675Q14.775,18.625 14.925,18.575Q15.075,18.525 15.225,18.45L12.625,15.8V22.025L6.45,15.85H1.95V8.15H5.2L0.55,3.35L2.2,1.7L21.725,21.725ZM20.5,17.075 L18.775,15.3Q19.2,14.55 19.413,13.712Q19.625,12.875 19.625,11.975Q19.625,9.65 18.225,7.812Q16.825,5.975 14.625,5.275V2.8Q17.875,3.525 19.95,6.087Q22.025,8.65 22.025,11.975Q22.025,13.4 21.625,14.675Q21.225,15.95 20.5,17.075ZM8.9,11.925ZM17.175,13.675 L14.625,11.075V7.65Q15.9,8.25 16.713,9.412Q17.525,10.575 17.525,12Q17.525,12.45 17.438,12.875Q17.35,13.3 17.175,13.675ZM12.625,9 L9.175,5.425 12.625,1.975ZM9.475,14.425V12.55L8.275,11.3H5.1V12.7H7.75Z"/>
|
||||||
|
</vector>
|
13
mastodon/src/main/res/layout/expanded_title_medium.xml
Normal file
13
mastodon/src/main/res/layout/expanded_title_medium.xml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textAppearance="@style/m3_headline_small"
|
||||||
|
android:textColor="?colorM3OnSurface"
|
||||||
|
android:paddingHorizontal="16dp"
|
||||||
|
android:paddingBottom="24dp"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:maxLines="2"
|
||||||
|
android:ellipsize="end"
|
||||||
|
tools:text="Very long title that does not fit on one line"/>
|
83
mastodon/src/main/res/layout/item_notification_request.xml
Normal file
83
mastodon/src/main/res/layout/item_notification_request.xml
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="16dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/ava"
|
||||||
|
android:layout_width="32dp"
|
||||||
|
android:layout_height="32dp"
|
||||||
|
android:layout_marginVertical="20dp"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
tools:src="#0f0"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/badge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_alignEnd="@id/ava"
|
||||||
|
android:layout_alignBottom="@id/ava"
|
||||||
|
android:layout_marginEnd="-4dp"
|
||||||
|
android:layout_marginBottom="-4dp"
|
||||||
|
android:minWidth="16dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:background="@drawable/bg_ava_badge"
|
||||||
|
android:paddingHorizontal="4dp"
|
||||||
|
android:textAppearance="@style/m3_label_small"
|
||||||
|
android:textColor="?colorM3OnPrimary"
|
||||||
|
tools:text="99+"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_allow"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
style="@style/Widget.Mastodon.M3.Button.Outlined"
|
||||||
|
android:contentDescription="@string/allow_notifications"
|
||||||
|
android:src="@drawable/ic_check_24px"/>
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/btn_mute"
|
||||||
|
android:layout_width="40dp"
|
||||||
|
android:layout_height="40dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_toStartOf="@id/btn_allow"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
style="@style/Widget.Mastodon.M3.Button.Outlined"
|
||||||
|
android:contentDescription="@string/mute_notifications"
|
||||||
|
android:src="@drawable/ic_volume_off_24px"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/name"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_toEndOf="@id/ava"
|
||||||
|
android:layout_toStartOf="@id/btn_mute"
|
||||||
|
android:layout_marginTop="14dp"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textAppearance="@style/m3_body_large"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textColor="?colorM3OnSurface"
|
||||||
|
tools:text="User Name"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/username"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="20dp"
|
||||||
|
android:layout_below="@id/name"
|
||||||
|
android:layout_toEndOf="@id/ava"
|
||||||
|
android:layout_toStartOf="@id/btn_mute"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:textAppearance="@style/m3_body_medium"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textColor="?colorM3OnSurfaceVariant"
|
||||||
|
tools:text="\@username@domain"/>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
15
mastodon/src/main/res/menu/notification_request.xml
Normal file
15
mastodon/src/main/res/menu/notification_request.xml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item
|
||||||
|
android:id="@+id/mute"
|
||||||
|
android:title="@string/mute_notifications"
|
||||||
|
android:icon="@drawable/ic_volume_off_24px"
|
||||||
|
android:checkable="true"
|
||||||
|
android:showAsAction="always"/>
|
||||||
|
<item
|
||||||
|
android:id="@+id/allow"
|
||||||
|
android:title="@string/allow_notifications"
|
||||||
|
android:icon="@drawable/ic_check_24px"
|
||||||
|
android:checkable="true"
|
||||||
|
android:showAsAction="always"/>
|
||||||
|
</menu>
|
|
@ -1,4 +1,13 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
<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"/>
|
<item
|
||||||
|
android:id="@+id/mark_all_read"
|
||||||
|
android:icon="@drawable/ic_done_all_24px"
|
||||||
|
android:showAsAction="always"
|
||||||
|
android:title="@string/mark_all_notifications_read" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/filters"
|
||||||
|
android:icon="@drawable/ic_tune_24px"
|
||||||
|
android:showAsAction="always"
|
||||||
|
android:title="@string/filter_notifications"/>
|
||||||
</menu>
|
</menu>
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
|
||||||
<string name="app_name" translatable="false">Mastodon</string>
|
<string name="app_name" translatable="false">Mastodon</string>
|
||||||
|
|
||||||
<string name="log_in">Log in</string>
|
<string name="log_in">Log in</string>
|
||||||
|
@ -728,4 +728,23 @@
|
||||||
<string name="mute_conversation">Mute conversation</string>
|
<string name="mute_conversation">Mute conversation</string>
|
||||||
<string name="unmute_conversation">Unmute conversation</string>
|
<string name="unmute_conversation">Unmute conversation</string>
|
||||||
<string name="visibility_unlisted">Quiet public</string>
|
<string name="visibility_unlisted">Quiet public</string>
|
||||||
|
<string name="filtered_notifications">Filtered notifications</string>
|
||||||
|
<string name="filter_notifications">Filter out notifications from...</string>
|
||||||
|
<string name="notification_filter_following">People you don’t follow</string>
|
||||||
|
<string name="notification_filter_following_explanation">Until you manually approve them</string>
|
||||||
|
<string name="notification_filter_followers">People not following you</string>
|
||||||
|
<string name="notification_filter_followers_explanation">Including people who have been following you fewer than 3 days</string>
|
||||||
|
<string name="notification_filter_new_accounts">New accounts</string>
|
||||||
|
<string name="notification_filter_new_accounts_explanation">Created within the past 30 days</string>
|
||||||
|
<string name="notification_filter_mentions">Unsolicited private mentions</string>
|
||||||
|
<string name="notification_filter_mentions_explanation">Filtered unless it’s in reply to your own mention or if you follow the sender</string>
|
||||||
|
<string name="allow_notifications">Allow notifications</string>
|
||||||
|
<string name="mute_notifications">Mute notifications</string>
|
||||||
|
<plurals name="x_people_you_may_know">
|
||||||
|
<item quantity="one">%,d person you may know</item>
|
||||||
|
<item quantity="other">%,d people you may know</item>
|
||||||
|
</plurals>
|
||||||
|
<string name="notifications_from_user">Notifications from %s</string>
|
||||||
|
<string name="notifications_muted">Notifications from %s will be muted.</string>
|
||||||
|
<string name="notifications_allowed">%s will now appear in your notification list.</string>
|
||||||
</resources>
|
</resources>
|
|
@ -215,6 +215,8 @@
|
||||||
|
|
||||||
<style name="Theme.Mastodon.Dialog.Alert" parent="android:Theme.Material.Light.Dialog.Alert">
|
<style name="Theme.Mastodon.Dialog.Alert" parent="android:Theme.Material.Light.Dialog.Alert">
|
||||||
<item name="android:windowTitleStyle">@style/alert_title</item>
|
<item name="android:windowTitleStyle">@style/alert_title</item>
|
||||||
|
<!-- it's unfortunate that https://cs.android.com/android/platform/superproject/main/+/main:frameworks/base/core/java/com/android/internal/widget/DialogTitle.java exists -->
|
||||||
|
<item name="android:textAppearanceMedium">@style/alert_title</item>
|
||||||
<item name="android:dialogPreferredPadding">24dp</item>
|
<item name="android:dialogPreferredPadding">24dp</item>
|
||||||
<item name="android:windowBackground">@drawable/bg_alert</item>
|
<item name="android:windowBackground">@drawable/bg_alert</item>
|
||||||
<item name="android:buttonBarButtonStyle">@style/Widget.Mastodon.ButtonBarButton</item>
|
<item name="android:buttonBarButtonStyle">@style/Widget.Mastodon.ButtonBarButton</item>
|
||||||
|
@ -229,6 +231,7 @@
|
||||||
|
|
||||||
<style name="Theme.Mastodon.Dialog.Alert.Dark" parent="android:Theme.Material.Dialog.Alert">
|
<style name="Theme.Mastodon.Dialog.Alert.Dark" parent="android:Theme.Material.Dialog.Alert">
|
||||||
<item name="android:windowTitleStyle">@style/alert_title</item>
|
<item name="android:windowTitleStyle">@style/alert_title</item>
|
||||||
|
<item name="android:textAppearanceMedium">@style/alert_title</item>
|
||||||
<item name="android:dialogPreferredPadding">24dp</item>
|
<item name="android:dialogPreferredPadding">24dp</item>
|
||||||
<item name="android:windowBackground">@drawable/bg_alert</item>
|
<item name="android:windowBackground">@drawable/bg_alert</item>
|
||||||
<item name="android:buttonBarButtonStyle">@style/Widget.Mastodon.ButtonBarButton</item>
|
<item name="android:buttonBarButtonStyle">@style/Widget.Mastodon.ButtonBarButton</item>
|
||||||
|
@ -314,6 +317,7 @@
|
||||||
<style name="Widget.Mastodon.M3.Button.Outlined">
|
<style name="Widget.Mastodon.M3.Button.Outlined">
|
||||||
<item name="android:background">@drawable/bg_button_m3_outlined</item>
|
<item name="android:background">@drawable/bg_button_m3_outlined</item>
|
||||||
<item name="android:textColor">@color/button_text_m3_text</item>
|
<item name="android:textColor">@color/button_text_m3_text</item>
|
||||||
|
<item name="android:tint">@color/action_bar_icons</item>
|
||||||
<item name="android:paddingLeft">24dp</item>
|
<item name="android:paddingLeft">24dp</item>
|
||||||
<item name="android:paddingRight">24dp</item>
|
<item name="android:paddingRight">24dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Add table
Reference in a new issue