From 521ba8766ce46e78e06ae3bbd50b17bed4e69868 Mon Sep 17 00:00:00 2001 From: sk Date: Thu, 17 Nov 2022 18:52:22 +0100 Subject: [PATCH] implement follow requests list closes #39 --- .../AuthorizeFollowRequest.java | 2 +- .../requests/accounts/GetFollowRequests.java | 50 +++ .../RejectFollowRequest.java | 2 +- .../events/FollowRequestHandledEvent.java | 18 + .../fragments/FollowRequestsListFragment.java | 344 ++++++++++++++++++ .../fragments/NotificationsFragment.java | 57 ++- .../fragments/NotificationsListFragment.java | 8 + .../android/ui/utils/UiUtils.java | 13 +- .../drawable/ic_follow_requests_24_badged.xml | 10 + mastodon/src/main/res/menu/notifications.xml | 8 + .../src/main/res/values-de-rDE/strings.xml | 1 + mastodon/src/main/res/values/strings.xml | 1 + 12 files changed, 505 insertions(+), 9 deletions(-) rename mastodon/src/main/java/org/joinmastodon/android/api/requests/{follow_requests => accounts}/AuthorizeFollowRequest.java (85%) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetFollowRequests.java rename mastodon/src/main/java/org/joinmastodon/android/api/requests/{follow_requests => accounts}/RejectFollowRequest.java (85%) create mode 100644 mastodon/src/main/java/org/joinmastodon/android/events/FollowRequestHandledEvent.java create mode 100644 mastodon/src/main/java/org/joinmastodon/android/fragments/FollowRequestsListFragment.java create mode 100644 mastodon/src/main/res/drawable/ic_follow_requests_24_badged.xml create mode 100644 mastodon/src/main/res/menu/notifications.xml diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/follow_requests/AuthorizeFollowRequest.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/AuthorizeFollowRequest.java similarity index 85% rename from mastodon/src/main/java/org/joinmastodon/android/api/requests/follow_requests/AuthorizeFollowRequest.java rename to mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/AuthorizeFollowRequest.java index 765ef7f77..13ca0fbbb 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/follow_requests/AuthorizeFollowRequest.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/AuthorizeFollowRequest.java @@ -1,4 +1,4 @@ -package org.joinmastodon.android.api.requests.follow_requests; +package org.joinmastodon.android.api.requests.accounts; import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.model.Relationship; diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetFollowRequests.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetFollowRequests.java new file mode 100644 index 000000000..0f231b905 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/GetFollowRequests.java @@ -0,0 +1,50 @@ +package org.joinmastodon.android.api.requests.accounts; + +import com.google.gson.reflect.TypeToken; + +import org.joinmastodon.android.api.MastodonAPIRequest; +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.FollowSuggestion; + +import java.io.IOException; +import java.util.List; + +import okhttp3.Response; + +public class GetFollowRequests extends MastodonAPIRequest>{ + private String maxId; + + public GetFollowRequests(String maxID, String minID, int limit){ + super(HttpMethod.GET, "/follow_requests", new TypeToken<>(){}); + if(maxID!=null) + addQueryParameter("max_id", maxID); + if(minID!=null) + addQueryParameter("min_id", minID); + if(limit>0) + addQueryParameter("limit", ""+limit); + } + + @Override + public void validateAndPostprocessResponse(List respObj, Response httpResponse) throws IOException { + super.validateAndPostprocessResponse(respObj, httpResponse); + // ; rel="next", + // ; rel="prev" + String link=httpResponse.header("link"); + // parsing link header by hand; using a library would be cleaner + // (also, the functionality should be part of the max id logics and implemented in MastodonAPIRequest) + if(link==null) return; + String maxIdEq="max_id="; + for(String s : link.split(",")) { + if(s.contains("rel=\"next\"")) { + int start=s.indexOf(maxIdEq)+maxIdEq.length(); + int end=s.indexOf('>'); + if(start<0 || start>end) return; + this.maxId=s.substring(start, end); + } + } + } + + public String getMaxId() { + return maxId; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/api/requests/follow_requests/RejectFollowRequest.java b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/RejectFollowRequest.java similarity index 85% rename from mastodon/src/main/java/org/joinmastodon/android/api/requests/follow_requests/RejectFollowRequest.java rename to mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/RejectFollowRequest.java index 23725795a..fd9ea1d2b 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/api/requests/follow_requests/RejectFollowRequest.java +++ b/mastodon/src/main/java/org/joinmastodon/android/api/requests/accounts/RejectFollowRequest.java @@ -1,4 +1,4 @@ -package org.joinmastodon.android.api.requests.follow_requests; +package org.joinmastodon.android.api.requests.accounts; import org.joinmastodon.android.api.MastodonAPIRequest; import org.joinmastodon.android.model.Relationship; diff --git a/mastodon/src/main/java/org/joinmastodon/android/events/FollowRequestHandledEvent.java b/mastodon/src/main/java/org/joinmastodon/android/events/FollowRequestHandledEvent.java new file mode 100644 index 000000000..84cf3ebd3 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/events/FollowRequestHandledEvent.java @@ -0,0 +1,18 @@ +package org.joinmastodon.android.events; + +import org.joinmastodon.android.model.Account; +import org.joinmastodon.android.model.Relationship; + +public class FollowRequestHandledEvent { + public String accountID; + public boolean accepted; + public Account account; + public Relationship relationship; + + public FollowRequestHandledEvent(String accountID, boolean accepted, Account account, Relationship rel){ + this.accountID=accountID; + this.accepted=accepted; + this.account=account; + this.relationship=rel; + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowRequestsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowRequestsListFragment.java new file mode 100644 index 000000000..6495bc4e7 --- /dev/null +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/FollowRequestsListFragment.java @@ -0,0 +1,344 @@ +package org.joinmastodon.android.fragments; + +import android.app.Activity; +import android.graphics.Rect; +import android.graphics.drawable.Animatable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +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.api.requests.accounts.GetAccountRelationships; +import org.joinmastodon.android.api.requests.accounts.GetFollowRequests; +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 org.parceler.Parcels; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +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.fragments.BaseRecyclerFragment; +import me.grishka.appkit.imageloader.ImageLoaderRecyclerAdapter; +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.BindableViewHolder; +import me.grishka.appkit.utils.V; +import me.grishka.appkit.views.UsableRecyclerView; + +public class FollowRequestsListFragment extends BaseRecyclerFragment implements ScrollableToTop{ + private String accountID; + private Map relationships=Collections.emptyMap(); + private GetAccountRelationships relationshipsRequest; + private String lastMaxId=null; + + public FollowRequestsListFragment(){ + super(20); + } + + @Override + public void onCreate(Bundle savedInstanceState){ + super.onCreate(savedInstanceState); + accountID=getArguments().getString("account"); + loadData(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + setTitle(R.string.follow_requests); + } + + @Override + protected void doLoadData(int offset, int count){ + if(relationshipsRequest!=null){ + relationshipsRequest.cancel(); + relationshipsRequest=null; + } + currentRequest=new GetFollowRequests(offset>0 ? lastMaxId : null, null, count) + .setCallback(new SimpleCallback<>(this){ + @Override + public void onSuccess(List result){ + onDataLoaded(result.stream().map(AccountWrapper::new).collect(Collectors.toList()), false); + loadRelationships(); + } + }) + .exec(accountID); + } + + @Override + protected RecyclerView.Adapter getAdapter(){ + return new AccountsAdapter(); + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState){ + super.onViewCreated(view, savedInstanceState); + list.addItemDecoration(new RecyclerView.ItemDecoration(){ + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){ + outRect.bottom=outRect.left=outRect.right=V.dp(16); + if(parent.getChildAdapterPosition(view)==0) + outRect.top=V.dp(16); + } + }); + ((UsableRecyclerView)list).setDrawSelectorOnTop(true); + } + + private void loadRelationships(){ + relationships=Collections.emptyMap(); + relationshipsRequest=new GetAccountRelationships(data.stream().map(fs->fs.account.id).collect(Collectors.toList())); + relationshipsRequest.setCallback(new Callback<>(){ + @Override + public void onSuccess(List result){ + relationshipsRequest=null; + relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity())); + if(list==null) + return; + for(int i=0;i implements ImageLoaderRecyclerAdapter{ + + public AccountsAdapter(){ + super(imgLoader); + } + + @Override + public void onBindViewHolder(AccountViewHolder holder, int position){ + holder.bind(data.get(position)); + super.onBindViewHolder(holder, position); + } + + @NonNull + @Override + public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){ + return new AccountViewHolder(); + } + + @Override + public int getItemCount(){ + return data.size(); + } + + @Override + public int getImageCountForItem(int position){ + return 2+data.get(position).emojiHelper.getImageCount(); + } + + @Override + public ImageLoaderRequest getImageRequest(int position, int image){ + AccountWrapper item=data.get(position); + if(image==0) + return item.avaRequest; + else if(image==1) + return item.coverRequest; + else + return item.emojiHelper.getImageRequest(image-2); + } + } + + // literally the same as AccountCardStatusDisplayItem and DiscoverAccountsFragment. code should be generalized + private class AccountViewHolder extends BindableViewHolder implements ImageLoaderViewHolder, UsableRecyclerView.Clickable{ + private final ImageView cover, avatar; + private final TextView name, username, bio, followersCount, followingCount, postsCount, followersLabel, followingLabel, postsLabel; + private final ProgressBarButton actionButton, acceptButton, rejectButton; + private final ProgressBar actionProgress, acceptProgress, rejectProgress; + private final View actionWrap, acceptWrap, rejectWrap; + + private Relationship relationship; + + public AccountViewHolder(){ + super(getActivity(), R.layout.item_discover_account, list); + 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); + acceptButton=findViewById(R.id.accept_btn); + acceptProgress=findViewById(R.id.accept_progress); + acceptWrap=findViewById(R.id.accept_btn_wrap); + rejectButton=findViewById(R.id.reject_btn); + rejectProgress=findViewById(R.id.reject_progress); + rejectWrap=findViewById(R.id.reject_btn_wrap); + + itemView.setOutlineProvider(OutlineProviders.roundedRect(6)); + itemView.setClipToOutline(true); + avatar.setOutlineProvider(OutlineProviders.roundedRect(12)); + avatar.setClipToOutline(true); + cover.setOutlineProvider(OutlineProviders.roundedRect(3)); + cover.setClipToOutline(true); + actionButton.setOnClickListener(this::onActionButtonClick); + acceptButton.setOnClickListener(this::onFollowRequestButtonClick); + rejectButton.setOnClickListener(this::onFollowRequestButtonClick); + } + + @Override + public void onBind(AccountWrapper 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(getResources().getQuantityString(R.plurals.followers, (int)Math.min(999, item.account.followersCount))); + followingLabel.setText(getResources().getQuantityString(R.plurals.following, (int)Math.min(999, item.account.followingCount))); + postsLabel.setText(getResources().getQuantityString(R.plurals.posts, (int)Math.min(999, item.account.statusesCount))); + relationship=relationships.get(item.account.id); + if(relationship == null || !relationship.followedBy){ + actionWrap.setVisibility(View.GONE); + acceptWrap.setVisibility(View.VISIBLE); + rejectWrap.setVisibility(View.VISIBLE); + + // i hate that i wasn't able to do this in xml + acceptButton.setCompoundDrawableTintList(acceptButton.getTextColors()); + acceptProgress.setIndeterminateTintList(acceptButton.getTextColors()); + rejectButton.setCompoundDrawableTintList(rejectButton.getTextColors()); + rejectProgress.setIndeterminateTintList(rejectButton.getTextColors()); + }else if(relationship==null){ + actionWrap.setVisibility(View.GONE); + acceptWrap.setVisibility(View.GONE); + rejectWrap.setVisibility(View.GONE); + }else{ + actionWrap.setVisibility(View.VISIBLE); + acceptWrap.setVisibility(View.GONE); + rejectWrap.setVisibility(View.GONE); + UiUtils.setRelationshipToActionButton(relationship, actionButton); + } + } + + @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 a && !a.isRunning()) + a.start(); + } + + @Override + public void clearImage(int index){ + setImage(index, null); + } + + @Override + public void onClick(){ + Bundle args=new Bundle(); + args.putString("account", accountID); + args.putParcelable("profileAccount", Parcels.wrap(item.account)); + Nav.go(getActivity(), ProfileFragment.class, args); + } + + private void onFollowRequestButtonClick(View v) { + itemView.setHasTransientState(true); + UiUtils.handleFollowRequest((Activity) v.getContext(), item.account, accountID, null, v == acceptButton, relationship, rel -> { + itemView.setHasTransientState(false); + relationships.put(item.account.id, rel); + RecyclerView.Adapter adapter = getBindingAdapter(); + if (!rel.requested && !rel.followedBy && adapter != null) { + data.remove(item); + adapter.notifyItemRemoved(getBindingAdapterPosition()); + } else { + rebind(); + } + }); + } + + private void onActionButtonClick(View v){ + itemView.setHasTransientState(true); + UiUtils.performAccountAction(getActivity(), item.account, accountID, relationship, actionButton, this::setActionProgressVisible, rel->{ + itemView.setHasTransientState(false); + relationships.put(item.account.id, rel); + rebind(); + }); + } + + private void setActionProgressVisible(boolean visible){ + actionButton.setTextVisible(!visible); + actionProgress.setVisibility(visible ? View.VISIBLE : View.GONE); + actionButton.setClickable(!visible); + } + } + + protected class AccountWrapper{ + public Account account; + public ImageLoaderRequest avaRequest, coverRequest; + public CustomEmojiHelper emojiHelper=new CustomEmojiHelper(); + public CharSequence parsedName, parsedBio; + + public AccountWrapper(Account account){ + 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(), accountID); + if(account.emojis.isEmpty()){ + parsedName=account.displayName; + }else{ + parsedName=HtmlParser.parseCustomEmoji(account.displayName, account.emojis); + emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio)); + } + } + } +} diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java index d8042db11..d164faefc 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsFragment.java @@ -2,16 +2,22 @@ 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.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.LinearLayout; +import org.joinmastodon.android.E; import org.joinmastodon.android.R; +import org.joinmastodon.android.api.requests.accounts.GetFollowRequests; +import org.joinmastodon.android.events.FollowRequestHandledEvent; +import org.joinmastodon.android.model.Account; import org.joinmastodon.android.ui.SimpleViewHolder; import org.joinmastodon.android.ui.tabs.TabLayout; import org.joinmastodon.android.ui.tabs.TabLayoutMediator; @@ -20,8 +26,15 @@ import org.joinmastodon.android.ui.utils.UiUtils; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import androidx.viewpager2.widget.ViewPager2; + +import com.squareup.otto.Subscribe; + +import java.util.List; + +import me.grishka.appkit.Nav; +import me.grishka.appkit.api.Callback; +import me.grishka.appkit.api.ErrorResponse; 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{ @@ -42,14 +55,36 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc setRetainInstance(true); accountID=getArguments().getString("account"); + E.register(this); + } + + @Override + public void onDestroy() { + super.onDestroy(); + E.unregister(this); } @Override public void onAttach(Activity activity){ super.onAttach(activity); + setHasOptionsMenu(true); setTitle(R.string.notifications); } + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){ + inflater.inflate(R.menu.notifications, menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() != R.id.follow_requests) return false; + Bundle args=new Bundle(); + args.putString("account", accountID); + Nav.go(getActivity(), FollowRequestsListFragment.class, args); + return true; + } + @Override public View onCreateContentView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState){ LinearLayout view=(LinearLayout) inflater.inflate(R.layout.fragment_notifications, container, false); @@ -123,12 +158,30 @@ public class NotificationsFragment extends MastodonToolbarFragment implements Sc return view; } + public void refreshFollowRequestsBadge() { + new GetFollowRequests(null, null, 1).setCallback(new Callback<>() { + @Override + public void onSuccess(List accounts) { + getToolbar().getMenu().findItem(R.id.follow_requests).setIcon(accounts.isEmpty() ? R.drawable.ic_fluent_person_add_24_regular : R.drawable.ic_follow_requests_24_badged); + } + + @Override + public void onError(ErrorResponse errorResponse) {} + }).exec(accountID); + } + + @Subscribe + public void onFollowRequestHandled(FollowRequestHandledEvent ev) { + refreshFollowRequestsBadge(); + } + @Override public void scrollToTop(){ getFragmentForPage(pager.getCurrentItem()).scrollToTop(); } public void loadData(){ + refreshFollowRequestsBadge(); if(allNotificationsFragment!=null && !allNotificationsFragment.loaded && !allNotificationsFragment.dataLoading) allNotificationsFragment.loadData(); } diff --git a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java index 59f44ff49..c4fad9e9a 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java +++ b/mastodon/src/main/java/org/joinmastodon/android/fragments/NotificationsListFragment.java @@ -55,6 +55,14 @@ public class NotificationsListFragment extends BaseStatusListFragment buildDisplayItems(Notification n){ String extraText=switch(n.type){ diff --git a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java index 9b93aebd2..35b6fc9ac 100644 --- a/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java +++ b/mastodon/src/main/java/org/joinmastodon/android/ui/utils/UiUtils.java @@ -11,7 +11,6 @@ import android.content.res.TypedArray; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.Canvas; -import android.graphics.Typeface; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.graphics.drawable.InsetDrawable; @@ -40,11 +39,12 @@ import org.joinmastodon.android.api.requests.accounts.SetAccountBlocked; import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed; import org.joinmastodon.android.api.requests.accounts.SetAccountMuted; import org.joinmastodon.android.api.requests.accounts.SetDomainBlocked; -import org.joinmastodon.android.api.requests.follow_requests.AuthorizeFollowRequest; -import org.joinmastodon.android.api.requests.follow_requests.RejectFollowRequest; +import org.joinmastodon.android.api.requests.accounts.AuthorizeFollowRequest; +import org.joinmastodon.android.api.requests.accounts.RejectFollowRequest; import org.joinmastodon.android.api.requests.statuses.DeleteStatus; import org.joinmastodon.android.api.requests.statuses.GetStatusByID; import org.joinmastodon.android.api.session.AccountSessionManager; +import org.joinmastodon.android.events.FollowRequestHandledEvent; import org.joinmastodon.android.events.NotificationDeletedEvent; import org.joinmastodon.android.events.StatusDeletedEvent; import org.joinmastodon.android.fragments.HashtagTimelineFragment; @@ -74,6 +74,7 @@ import java.util.function.Consumer; import java.util.stream.Collectors; import androidx.annotation.AttrRes; +import androidx.annotation.Nullable; import androidx.annotation.StringRes; import androidx.browser.customtabs.CustomTabsIntent; import androidx.recyclerview.widget.DiffUtil; @@ -464,11 +465,12 @@ public class UiUtils{ } - public static void handleFollowRequest(Activity activity, Account account, String accountID, String notificationID, boolean accepted, Relationship relationship, Consumer resultCallback) { + public static void handleFollowRequest(Activity activity, Account account, String accountID, @Nullable String notificationID, boolean accepted, Relationship relationship, Consumer resultCallback) { if (accepted) { new AuthorizeFollowRequest(account.id).setCallback(new Callback<>() { @Override public void onSuccess(Relationship rel) { + E.post(new FollowRequestHandledEvent(accountID, true, account, rel)); resultCallback.accept(rel); } @@ -482,7 +484,8 @@ public class UiUtils{ new RejectFollowRequest(account.id).setCallback(new Callback<>() { @Override public void onSuccess(Relationship rel) { - E.post(new NotificationDeletedEvent(notificationID)); + E.post(new FollowRequestHandledEvent(accountID, false, account, rel)); + if (notificationID != null) E.post(new NotificationDeletedEvent(notificationID)); resultCallback.accept(rel); } diff --git a/mastodon/src/main/res/drawable/ic_follow_requests_24_badged.xml b/mastodon/src/main/res/drawable/ic_follow_requests_24_badged.xml new file mode 100644 index 000000000..dfead8a2c --- /dev/null +++ b/mastodon/src/main/res/drawable/ic_follow_requests_24_badged.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/menu/notifications.xml b/mastodon/src/main/res/menu/notifications.xml new file mode 100644 index 000000000..06266f432 --- /dev/null +++ b/mastodon/src/main/res/menu/notifications.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/mastodon/src/main/res/values-de-rDE/strings.xml b/mastodon/src/main/res/values-de-rDE/strings.xml index f406d93bf..6c83bb52d 100644 --- a/mastodon/src/main/res/values-de-rDE/strings.xml +++ b/mastodon/src/main/res/values-de-rDE/strings.xml @@ -377,6 +377,7 @@ Download (%s) Installieren + Folgeanfragen Folgeanfrage akzeptieren Folgeanfrage ablehnen diff --git a/mastodon/src/main/res/values/strings.xml b/mastodon/src/main/res/values/strings.xml index 8023dfef2..9266d0f43 100644 --- a/mastodon/src/main/res/values/strings.xml +++ b/mastodon/src/main/res/values/strings.xml @@ -387,6 +387,7 @@ Mastodon and your privacy Although the Mastodon app does not collect any data, the server you sign up through may have a different policy. Take a minute to review and agree to the Mastodon app privacy policy and your server\'s privacy policy. I Agree + Follow requests Accept follow request Reject follow request