From 5f1cd065661e7868b7b96eaf707eb6fed919505a Mon Sep 17 00:00:00 2001 From: Thomas Date: Tue, 22 Nov 2022 14:50:21 +0100 Subject: [PATCH] Fix issue #513 - Add the ability to check blocked domains and to unblock them. --- .../android/activities/ActionActivity.java | 18 ++ .../endpoints/MastodonAccountsService.java | 2 +- .../android/client/entities/api/Domains.java | 22 ++ .../android/client/entities/app/Timeline.java | 2 + .../android/ui/drawer/DomainBlockAdapter.java | 95 ++++++++ .../timeline/FragmentMastodonDomainBlock.java | 214 ++++++++++++++++++ .../viewmodel/mastodon/AccountsVM.java | 18 +- .../main/res/layout/drawer_domain_block.xml | 53 +++++ app/src/main/res/values/strings.xml | 3 + 9 files changed, 418 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/app/fedilab/android/client/entities/api/Domains.java create mode 100644 app/src/main/java/app/fedilab/android/ui/drawer/DomainBlockAdapter.java create mode 100644 app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonDomainBlock.java create mode 100644 app/src/main/res/layout/drawer_domain_block.xml diff --git a/app/src/main/java/app/fedilab/android/activities/ActionActivity.java b/app/src/main/java/app/fedilab/android/activities/ActionActivity.java index 7ff2e7b43..0088f7c37 100644 --- a/app/src/main/java/app/fedilab/android/activities/ActionActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/ActionActivity.java @@ -29,6 +29,7 @@ import app.fedilab.android.databinding.ActivityActionsBinding; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.ThemeHelper; import app.fedilab.android.ui.fragment.timeline.FragmentMastodonAccount; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonDomainBlock; import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; public class ActionActivity extends BaseActivity { @@ -37,6 +38,7 @@ public class ActionActivity extends BaseActivity { private boolean canGoBack; private FragmentMastodonTimeline fragmentMastodonTimeline; private FragmentMastodonAccount fragmentMastodonAccount; + private FragmentMastodonDomainBlock fragmentMastodonDomainBlock; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -54,6 +56,7 @@ public class ActionActivity extends BaseActivity { binding.bookmarks.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.BOOKMARK_TIMELINE)); binding.muted.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.MUTED_TIMELINE)); binding.blocked.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.BLOCKED_TIMELINE)); + binding.domainBlock.setOnClickListener(v -> displayTimeline(Timeline.TimeLineEnum.BLOCKED_DOMAIN_TIMELINE)); } private void displayTimeline(Timeline.TimeLineEnum type) { @@ -73,6 +76,15 @@ public class ActionActivity extends BaseActivity { fragmentTransaction.commit(); }); + } else if (type == Timeline.TimeLineEnum.BLOCKED_DOMAIN_TIMELINE) { + ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> { + fragmentMastodonDomainBlock = new FragmentMastodonDomainBlock(); + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fragmentTransaction = + fragmentManager.beginTransaction(); + fragmentTransaction.replace(R.id.fragment_container, fragmentMastodonDomainBlock); + fragmentTransaction.commit(); + }); } else { ThemeHelper.slideViewsToLeft(binding.buttonContainer, binding.fragmentContainer, () -> { @@ -102,6 +114,9 @@ public class ActionActivity extends BaseActivity { case BOOKMARK_TIMELINE: setTitle(R.string.bookmarks); break; + case BLOCKED_DOMAIN_TIMELINE: + setTitle(R.string.blocked_domains); + break; } } @@ -116,6 +131,9 @@ public class ActionActivity extends BaseActivity { if (fragmentMastodonAccount != null) { fragmentMastodonAccount.onDestroyView(); } + if (fragmentMastodonDomainBlock != null) { + fragmentMastodonDomainBlock.onDestroyView(); + } }); setTitle(R.string.interactions); } else { diff --git a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAccountsService.java b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAccountsService.java index 499988c2b..a2ec0d5f3 100644 --- a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAccountsService.java +++ b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonAccountsService.java @@ -316,7 +316,7 @@ public interface MastodonAccountsService { @DELETE("domain_blocks") Call removeDomainBlocks( @Header("Authorization") String token, - @Field("domain") String domain + @Query("domain") String domain ); diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Domains.java b/app/src/main/java/app/fedilab/android/client/entities/api/Domains.java new file mode 100644 index 000000000..cbcb63c38 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Domains.java @@ -0,0 +1,22 @@ +package app.fedilab.android.client.entities.api; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import java.util.List; + +public class Domains { + public Pagination pagination = new Pagination(); + public List domains; +} diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java b/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java index a195daaa9..0f5f6046b 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java +++ b/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java @@ -390,6 +390,8 @@ public class Timeline { MUTED_TIMELINE("MUTED_TIMELINE"), @SerializedName("BOOKMARK_TIMELINE") BOOKMARK_TIMELINE("BOOKMARK_TIMELINE"), + @SerializedName("BLOCKED_DOMAIN_TIMELINE") + BLOCKED_DOMAIN_TIMELINE("BLOCKED_DOMAIN_TIMELINE"), @SerializedName("BLOCKED_TIMELINE") BLOCKED_TIMELINE("BLOCKED_TIMELINE"), @SerializedName("FAVOURITE_TIMELINE") diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/DomainBlockAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/DomainBlockAdapter.java new file mode 100644 index 000000000..a539f5e3e --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/DomainBlockAdapter.java @@ -0,0 +1,95 @@ +package app.fedilab.android.ui.drawer; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.lifecycle.ViewModelProvider; +import androidx.lifecycle.ViewModelStoreOwner; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.activities.MainActivity; +import app.fedilab.android.databinding.DrawerDomainBlockBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; + + +public class DomainBlockAdapter extends RecyclerView.Adapter { + private final List domainList; + private Context context; + + public DomainBlockAdapter(List domainList) { + this.domainList = domainList; + } + + public int getCount() { + return domainList.size(); + } + + public String getItem(int position) { + return domainList.get(position); + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + context = parent.getContext(); + DrawerDomainBlockBinding itemBinding = DrawerDomainBlockBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new DomainBlockViewHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { + String domain = domainList.get(position); + DomainBlockViewHolder holder = (DomainBlockViewHolder) viewHolder; + holder.binding.domainName.setText(domain); + AccountsVM accountsVM = new ViewModelProvider((ViewModelStoreOwner) context).get(AccountsVM.class); + holder.binding.unblockDomain.setOnClickListener(v -> { + AlertDialog.Builder alt_bld = new AlertDialog.Builder(context, Helper.dialogStyle()); + alt_bld.setMessage(context.getString(R.string.unblock_domain_confirm, domain)); + alt_bld.setPositiveButton(R.string.yes, (dialog, id) -> { + accountsVM.removeDomainBlocks(MainActivity.currentInstance, MainActivity.currentToken, domain); + domainList.remove(position); + notifyItemRemoved(position); + dialog.dismiss(); + }); + alt_bld.setNegativeButton(R.string.cancel, (dialog, id) -> dialog.dismiss()); + AlertDialog alert = alt_bld.create(); + alert.show(); + }); + } + + @Override + public int getItemCount() { + return domainList.size(); + } + + + public static class DomainBlockViewHolder extends RecyclerView.ViewHolder { + DrawerDomainBlockBinding binding; + + DomainBlockViewHolder(DrawerDomainBlockBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } +} diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonDomainBlock.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonDomainBlock.java new file mode 100644 index 000000000..65209c2fb --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonDomainBlock.java @@ -0,0 +1,214 @@ +package app.fedilab.android.ui.fragment.timeline; +/* Copyright 2022 Thomas Schneider + * + * This file is a part of Fedilab + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Fedilab; if not, + * see . */ + + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.ArrayList; +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.activities.MainActivity; +import app.fedilab.android.client.entities.api.Domains; +import app.fedilab.android.databinding.FragmentPaginationBinding; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.drawer.DomainBlockAdapter; +import app.fedilab.android.viewmodel.mastodon.AccountsVM; + + +public class FragmentMastodonDomainBlock extends Fragment { + + + private FragmentPaginationBinding binding; + private DomainBlockAdapter domainBlockAdapter; + private AccountsVM accountsVM; + private List domainList; + private boolean flagLoading; + private String max_id; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + + binding = FragmentPaginationBinding.inflate(inflater, container, false); + binding.getRoot().setBackgroundColor(ThemeHelper.getBackgroundColor(requireActivity())); + return binding.getRoot(); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + int c1 = getResources().getColor(R.color.cyanea_accent_reference); + binding.swipeContainer.setProgressBackgroundColorSchemeColor(getResources().getColor(R.color.cyanea_primary_reference)); + binding.swipeContainer.setColorSchemeColors( + c1, c1, c1 + ); + binding.loader.setVisibility(View.VISIBLE); + binding.recyclerView.setVisibility(View.GONE); + accountsVM = new ViewModelProvider(FragmentMastodonDomainBlock.this).get(AccountsVM.class); + flagLoading = false; + max_id = null; + domainList = new ArrayList<>(); + + binding.swipeContainer.setOnRefreshListener(() -> { + binding.swipeContainer.setRefreshing(true); + flagLoading = false; + max_id = null; + int size = domainList.size(); + domainList.clear(); + domainList = new ArrayList<>(); + domainBlockAdapter.notifyItemRangeRemoved(0, size); + router(); + }); + domainBlockAdapter = new DomainBlockAdapter(domainList); + LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity()); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(domainBlockAdapter); + binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition(); + if (dy > 0) { + int visibleItemCount = mLayoutManager.getChildCount(); + int totalItemCount = mLayoutManager.getItemCount(); + if (firstVisibleItem + visibleItemCount == totalItemCount) { + if (!flagLoading) { + flagLoading = true; + binding.loadingNextElements.setVisibility(View.VISIBLE); + router(); + } + } else { + binding.loadingNextElements.setVisibility(View.GONE); + } + } + + } + }); + router(); + } + + /** + * Router for timelines + */ + private void router() { + + if (max_id == null) { + accountsVM.getDomainBlocks(MainActivity.currentInstance, MainActivity.currentToken, null, null, null) + .observe(getViewLifecycleOwner(), this::initializeTagCommonView); + } else { + accountsVM.getDomainBlocks(MainActivity.currentInstance, MainActivity.currentToken, null, max_id, null) + .observe(getViewLifecycleOwner(), this::dealWithPagination); + } + } + + public void scrollToTop() { + binding.recyclerView.setAdapter(domainBlockAdapter); + } + + /** + * Intialize the view for domains + * + * @param domains List of {@link String} + */ + private void initializeTagCommonView(final Domains domains) { + flagLoading = false; + if (binding == null || !isAdded() || getActivity() == null) { + return; + } + binding.loader.setVisibility(View.GONE); + binding.noAction.setVisibility(View.GONE); + binding.swipeContainer.setRefreshing(false); + binding.swipeContainer.setOnRefreshListener(() -> { + binding.swipeContainer.setRefreshing(true); + flagLoading = false; + max_id = null; + router(); + }); + if (domains == null || domains.domains == null || domains.domains.size() == 0) { + binding.noAction.setVisibility(View.VISIBLE); + binding.noActionText.setText(R.string.no_accounts); + return; + } + binding.recyclerView.setVisibility(View.VISIBLE); + if (domainBlockAdapter != null && this.domainList != null) { + int size = this.domainList.size(); + this.domainList.clear(); + this.domainList = new ArrayList<>(); + domainBlockAdapter.notifyItemRangeRemoved(0, size); + } + + this.domainList = domains.domains; + domainBlockAdapter = new DomainBlockAdapter(this.domainList); + flagLoading = domains.pagination.max_id == null; + LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity()); + binding.recyclerView.setLayoutManager(mLayoutManager); + binding.recyclerView.setAdapter(domainBlockAdapter); + + max_id = domains.pagination.max_id; + binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition(); + if (dy > 0) { + int visibleItemCount = mLayoutManager.getChildCount(); + int totalItemCount = mLayoutManager.getItemCount(); + if (firstVisibleItem + visibleItemCount == totalItemCount) { + if (!flagLoading) { + flagLoading = true; + binding.loadingNextElements.setVisibility(View.VISIBLE); + router(); + } + } else { + binding.loadingNextElements.setVisibility(View.GONE); + } + } + + } + }); + } + + + private void dealWithPagination(Domains fetched_domains) { + flagLoading = false; + if (binding == null || !isAdded() || getActivity() == null) { + return; + } + binding.loadingNextElements.setVisibility(View.GONE); + if (domainList != null && fetched_domains != null && fetched_domains.domains != null) { + flagLoading = fetched_domains.pagination.max_id == null; + int startId = 0; + //There are some domains present in the timeline + if (domainList.size() > 0) { + startId = domainList.size(); + } + int position = domainList.size(); + domainList.addAll(fetched_domains.domains); + max_id = fetched_domains.pagination.max_id; + domainBlockAdapter.notifyItemRangeInserted(startId, fetched_domains.domains.size()); + } else { + flagLoading = true; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AccountsVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AccountsVM.java index 627f6f5df..f3ba0615d 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AccountsVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/AccountsVM.java @@ -35,6 +35,7 @@ import app.fedilab.android.activities.MainActivity; import app.fedilab.android.client.endpoints.MastodonAccountsService; import app.fedilab.android.client.entities.api.Account; import app.fedilab.android.client.entities.api.Accounts; +import app.fedilab.android.client.entities.api.Domains; import app.fedilab.android.client.entities.api.FeaturedTag; import app.fedilab.android.client.entities.api.Field; import app.fedilab.android.client.entities.api.Filter; @@ -89,7 +90,7 @@ public class AccountsVM extends AndroidViewModel { private MutableLiveData> tagListMutableLiveData; private MutableLiveData preferencesMutableLiveData; private MutableLiveData tokenMutableLiveData; - private MutableLiveData> stringListMutableLiveData; + private MutableLiveData domainsMutableLiveData; private MutableLiveData reportMutableLiveData; public AccountsVM(@NonNull Application application) { @@ -1029,11 +1030,12 @@ public class AccountsVM extends AndroidViewModel { * View domains the user has blocked. * * @param limit Maximum number of results. Defaults to 40. - * @return {@link LiveData} containing a {@link List} of {@link String}s + * @return {@link LiveData} containing {@link Domains} */ - public LiveData> getDomainBlocks(@NonNull String instance, String token, String limit, String maxId, String sinceId) { - stringListMutableLiveData = new MutableLiveData<>(); + public LiveData getDomainBlocks(@NonNull String instance, String token, String limit, String maxId, String sinceId) { + domainsMutableLiveData = new MutableLiveData<>(); MastodonAccountsService mastodonAccountsService = init(instance); + Domains domains = new Domains(); new Thread(() -> { List stringList = null; Call> getDomainBlocksCall = mastodonAccountsService.getDomainBlocks(token, limit, maxId, sinceId); @@ -1041,18 +1043,18 @@ public class AccountsVM extends AndroidViewModel { try { Response> getDomainBlocksResponse = getDomainBlocksCall.execute(); if (getDomainBlocksResponse.isSuccessful()) { - stringList = getDomainBlocksResponse.body(); + domains.domains = getDomainBlocksResponse.body(); + domains.pagination = MastodonHelper.getPagination(getDomainBlocksResponse.headers()); } } catch (Exception e) { e.printStackTrace(); } } Handler mainHandler = new Handler(Looper.getMainLooper()); - List finalStringList = stringList; - Runnable myRunnable = () -> stringListMutableLiveData.setValue(finalStringList); + Runnable myRunnable = () -> domainsMutableLiveData.setValue(domains); mainHandler.post(myRunnable); }).start(); - return stringListMutableLiveData; + return domainsMutableLiveData; } /** diff --git a/app/src/main/res/layout/drawer_domain_block.xml b/app/src/main/res/layout/drawer_domain_block.xml new file mode 100644 index 000000000..0207a1a3d --- /dev/null +++ b/app/src/main/res/layout/drawer_domain_block.xml @@ -0,0 +1,53 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c332f3bf3..9bf26a2f8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1907,4 +1907,7 @@ Suggestions Not interested Blocked domains + Unblock domain + You have not blocked domains + Are you sure to unblock %1$s? \ No newline at end of file