Skip to content

Commit

Permalink
feat: add mute notification toggle to mute dialog
Browse files Browse the repository at this point in the history
Closes sk22#966, by bringing parity with the web platform.
  • Loading branch information
FineFindus committed Jan 14, 2024
1 parent f7dfebc commit 137d584
Show file tree
Hide file tree
Showing 5 changed files with 381 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@
import org.joinmastodon.android.model.Relationship;

public class SetAccountMuted extends MastodonAPIRequest<Relationship>{
public SetAccountMuted(String id, boolean muted, long duration){
public SetAccountMuted(String id, boolean muted, long duration, boolean muteNotifications){
super(HttpMethod.POST, "/accounts/"+id+"/"+(muted ? "mute" : "unmute"), Relationship.class);
setRequestBody(new Request(duration));
setRequestBody(new Request(duration, muteNotifications));
}

private static class Request{
public long duration;
public Request(long duration){
public boolean muteNotifications;
public Request(long duration, boolean muteNotifications){
this.duration=duration;
this.muteNotifications=muteNotifications;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,340 @@
package org.joinmastodon.android.fragments.discover;

import android.graphics.Rect;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
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.GetFollowSuggestions;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.fragments.IsOnTop;
import org.joinmastodon.android.fragments.MastodonRecyclerFragment;
import org.joinmastodon.android.fragments.ProfileFragment;
import org.joinmastodon.android.fragments.ScrollableToTop;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.FollowSuggestion;
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.joinmastodon.android.utils.ProvidesAssistContent;
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.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 DiscoverAccountsFragment extends MastodonRecyclerFragment<DiscoverAccountsFragment.AccountWrapper> implements ScrollableToTop, IsOnTop, ProvidesAssistContent.ProvidesWebUri {
private String accountID;
private Map<String, Relationship> relationships=Collections.emptyMap();
private GetAccountRelationships relationshipsRequest;

public DiscoverAccountsFragment(){
super(20);
}

@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N)
setRetainInstance(true);
}

@Override
protected void doLoadData(int offset, int count){
if(relationshipsRequest!=null){
relationshipsRequest.cancel();
relationshipsRequest=null;
}
currentRequest=new GetFollowSuggestions(count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<FollowSuggestion> result){
if(getActivity()==null) return;
onDataLoaded(result.stream().map(fs->new AccountWrapper(fs.account)).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<Relationship> result){
relationshipsRequest=null;
relationships=result.stream().collect(Collectors.toMap(rel->rel.id, Function.identity()));
if(getActivity()==null) return;
if(list==null)
return;
for(int i=0;i<list.getChildCount();i++){
RecyclerView.ViewHolder holder=list.getChildViewHolder(list.getChildAt(i));
if(holder instanceof AccountViewHolder avh)
avh.rebind();
}
}

@Override
public void onError(ErrorResponse error){
relationshipsRequest=null;
}
}).exec(accountID);
}

@Override
public void onDestroyView(){
super.onDestroyView();
if(relationshipsRequest!=null){
relationshipsRequest.cancel();
relationshipsRequest=null;
}
}

@Override
public void scrollToTop(){
smoothScrollRecyclerViewToTop(list);
}

@Override
public boolean isOnTop() {
return isRecyclerViewOnTop(list);
}

@Override
public String getAccountID() {
return accountID;
}

@Override
public Uri getWebUri(Uri.Builder base) {
return isInstanceAkkoma() ? null : base.path("/explore/suggestions").build();
}

private class AccountsAdapter extends UsableRecyclerView.Adapter<AccountViewHolder> 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);
}
}

private class AccountViewHolder extends BindableViewHolder<AccountWrapper> implements ImageLoaderViewHolder, UsableRecyclerView.DisableableClickable{
private final ImageView cover, avatar;
private final TextView name, username, bio, followersCount, followingCount, postsCount, followersLabel, followingLabel, postsLabel;
private final ProgressBarButton actionButton;
private final ProgressBar actionProgress;
private final View actionWrap;

private Relationship relationship;

public 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);

avatar.setOutlineProvider(OutlineProviders.roundedRect(15));
avatar.setClipToOutline(true);
View border=findViewById(R.id.avatar_border);
border.setOutlineProvider(OutlineProviders.roundedRect(17));
border.setClipToOutline(true);
cover.setOutlineProvider(OutlineProviders.roundedRect(9));
cover.setClipToOutline(true);
itemView.setOutlineProvider(OutlineProviders.roundedRect(12));
itemView.setClipToOutline(true);
actionButton.setOnClickListener(this::onActionButtonClick);
itemView.setOnClickListener(v->this.onClick());
}

@Override
public boolean isEnabled(){
return false;
}

@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.sk_posts_count_label, (int)(item.account.statusesCount%1000), item.account.statusesCount));
followersCount.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followersLabel.setVisibility(item.account.followersCount < 0 ? View.GONE : View.VISIBLE);
followingCount.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
followingLabel.setVisibility(item.account.followingCount < 0 ? View.GONE : View.VISIBLE);
relationship=relationships.get(item.account.id);
UiUtils.setExtraTextInfo(getContext(), null, true, false, false, item.account);

if(relationship==null){
actionWrap.setVisibility(View.GONE);
}else{
actionWrap.setVisibility(View.VISIBLE);
UiUtils.setRelationshipToActionButtonM3(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 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;
avaRequest=new UrlImageLoaderRequest(
TextUtils.isEmpty(account.avatar) ? AccountSessionManager.getInstance().getAccount(accountID).getDefaultAvatarUrl() : 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.getDisplayName();
}else{
parsedName=HtmlParser.parseCustomEmoji(account.getDisplayName(), account.emojis);
emojiHelper.setText(new SpannableStringBuilder(parsedName).append(parsedBio));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupMenu;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;

Expand Down Expand Up @@ -567,6 +568,8 @@ public static void confirmToggleMuteUser(Context context, String accountID, Acco
durationView.setLayoutParams(params);
Button button=durationView.findViewById(R.id.button);
((TextView) durationView.findViewById(R.id.message)).setText(context.getString(R.string.confirm_mute, account.getDisplayName()));
Switch muteNotificationsSwitch=durationView.findViewById(R.id.mute_notifications_switch);
durationView.findViewById(R.id.mute_notifications).setOnClickListener(l->muteNotificationsSwitch.setChecked(!muteNotificationsSwitch.isChecked()));

AtomicReference<Duration> muteDuration=new AtomicReference<>(Duration.ZERO);

Expand Down Expand Up @@ -603,7 +606,7 @@ else if(id==R.id.duration_minutes_5){
.setMessage(currentlyMuted ? context.getString(R.string.confirm_unmute, account.getDisplayName()) : null)
.setView(currentlyMuted ? null : durationView)
.setPositiveButton(context.getString(currentlyMuted ? R.string.do_unmute : R.string.do_mute), (dlg, i)->{
new SetAccountMuted(account.id, !currentlyMuted, muteDuration.get().getSeconds())
new SetAccountMuted(account.id, !currentlyMuted, muteDuration.get().getSeconds(), muteNotificationsSwitch.isChecked())
.setCallback(new Callback<>(){
@Override
public void onSuccess(Relationship result){
Expand Down
Loading

0 comments on commit 137d584

Please sign in to comment.