diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActionListener.java b/app/src/main/java/com/keylesspalace/tusky/AccountActionListener.java index fb1a54530..e45cbc932 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActionListener.java @@ -17,4 +17,5 @@ package com.keylesspalace.tusky; public interface AccountActionListener { void onViewAccount(String id); + void onBlock(final boolean block, final String id, final int position); } diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountAdapter.java b/app/src/main/java/com/keylesspalace/tusky/AccountAdapter.java index 2bf69fc26..6fd60a6fd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountAdapter.java @@ -15,152 +15,59 @@ package com.keylesspalace.tusky; -import android.content.Context; import android.support.v7.widget.RecyclerView; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; - -import com.android.volley.toolbox.ImageLoader; -import com.android.volley.toolbox.NetworkImageView; import java.util.ArrayList; import java.util.List; -public class AccountAdapter extends RecyclerView.Adapter { - private static final int VIEW_TYPE_ACCOUNT = 0; - private static final int VIEW_TYPE_FOOTER = 1; +abstract class AccountAdapter extends RecyclerView.Adapter { + List accountList; + AccountActionListener accountActionListener; + FooterActionListener footerActionListener; + FooterViewHolder.State footerState; - private List accounts; - private AccountActionListener accountActionListener; - private FooterActionListener footerActionListener; - private FooterViewHolder.State footerState; - - public AccountAdapter(AccountActionListener accountActionListener, + AccountAdapter(AccountActionListener accountActionListener, FooterActionListener footerActionListener) { super(); - accounts = new ArrayList<>(); + accountList = new ArrayList<>(); this.accountActionListener = accountActionListener; this.footerActionListener = footerActionListener; footerState = FooterViewHolder.State.LOADING; } - @Override - public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { - switch (viewType) { - default: - case VIEW_TYPE_ACCOUNT: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_account, parent, false); - return new AccountViewHolder(view); - } - case VIEW_TYPE_FOOTER: { - View view = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.item_footer, parent, false); - return new FooterViewHolder(view); - } - } - } - - @Override - public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { - if (position < accounts.size()) { - AccountViewHolder holder = (AccountViewHolder) viewHolder; - holder.setupWithAccount(accounts.get(position)); - holder.setupActionListener(accountActionListener); - } else { - FooterViewHolder holder = (FooterViewHolder) viewHolder; - holder.setState(footerState); - holder.setupButton(footerActionListener); - holder.setRetryMessage(R.string.footer_retry_accounts); - holder.setEndOfTimelineMessage(R.string.footer_end_of_accounts); - } - } - @Override public int getItemCount() { - return accounts.size() + 1; + return accountList.size() + 1; } - @Override - public int getItemViewType(int position) { - if (position == accounts.size()) { - return VIEW_TYPE_FOOTER; + void update(List newAccounts) { + if (accountList == null || accountList.isEmpty()) { + accountList = newAccounts; } else { - return VIEW_TYPE_ACCOUNT; - } - } - - public void update(List newAccounts) { - if (accounts == null || accounts.isEmpty()) { - accounts = newAccounts; - } else { - int index = newAccounts.indexOf(accounts.get(0)); + int index = newAccounts.indexOf(accountList.get(0)); if (index == -1) { - accounts.addAll(0, newAccounts); + accountList.addAll(0, newAccounts); } else { - accounts.addAll(0, newAccounts.subList(0, index)); + accountList.addAll(0, newAccounts.subList(0, index)); } } notifyDataSetChanged(); } - public void addItems(List newAccounts) { - int end = accounts.size(); - accounts.addAll(newAccounts); + void addItems(List newAccounts) { + int end = accountList.size(); + accountList.addAll(newAccounts); notifyItemRangeInserted(end, newAccounts.size()); } public Account getItem(int position) { - if (position >= 0 && position < accounts.size()) { - return accounts.get(position); + if (position >= 0 && position < accountList.size()) { + return accountList.get(position); } return null; } - public void setFooterState(FooterViewHolder.State state) { + void setFooterState(FooterViewHolder.State state) { this.footerState = state; } - - private static class AccountViewHolder extends RecyclerView.ViewHolder { - private View container; - private TextView username; - private TextView displayName; - private TextView note; - private NetworkImageView avatar; - private String id; - - public AccountViewHolder(View itemView) { - super(itemView); - container = itemView.findViewById(R.id.account_container); - username = (TextView) itemView.findViewById(R.id.account_username); - displayName = (TextView) itemView.findViewById(R.id.account_display_name); - note = (TextView) itemView.findViewById(R.id.account_note); - avatar = (NetworkImageView) itemView.findViewById(R.id.account_avatar); - avatar.setDefaultImageResId(R.drawable.avatar_default); - avatar.setErrorImageResId(R.drawable.avatar_error); - } - - public void setupWithAccount(Account account) { - id = account.id; - String format = username.getContext().getString(R.string.status_username_format); - String formattedUsername = String.format(format, account.username); - username.setText(formattedUsername); - displayName.setText(account.displayName); - note.setText(account.note); - Context context = avatar.getContext(); - ImageLoader imageLoader = VolleySingleton.getInstance(context).getImageLoader(); - avatar.setImageUrl(account.avatar, imageLoader); - } - - public void setupActionListener(final AccountActionListener listener) { - container.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - listener.onViewAccount(id); - } - }); - } - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java b/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java index 1742188d2..750ed507d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java @@ -31,9 +31,11 @@ import android.view.View; import android.view.ViewGroup; import com.android.volley.AuthFailureError; +import com.android.volley.Request; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.JsonArrayRequest; +import com.android.volley.toolbox.StringRequest; import org.json.JSONArray; import org.json.JSONException; @@ -123,7 +125,11 @@ public class AccountFragment extends Fragment implements AccountActionListener, } }; recyclerView.addOnScrollListener(scrollListener); - adapter = new AccountAdapter(this, this); + if (type == Type.BLOCKS) { + adapter = new BlocksAdapter(this, this); + } else { + adapter = new FollowAdapter(this, this); + } recyclerView.setAdapter(adapter); if (jumpToTopAllowed()) { @@ -266,6 +272,52 @@ public class AccountFragment extends Fragment implements AccountActionListener, startActivity(intent); } + public void onBlock(final boolean block, final String id, final int position) { + String endpoint; + if (!block) { + endpoint = String.format(getString(R.string.endpoint_unblock), id); + } else { + endpoint = String.format(getString(R.string.endpoint_block), id); + } + String url = "https://" + domain + endpoint; + StringRequest request = new StringRequest(Request.Method.POST, url, + new Response.Listener() { + @Override + public void onResponse(String response) { + onBlockSuccess(block, position); + } + }, + new Response.ErrorListener() { + @Override + public void onErrorResponse(VolleyError error) { + onBlockFailure(block, id); + } + }) { + @Override + public Map getHeaders() throws AuthFailureError { + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + accessToken); + return headers; + } + }; + VolleySingleton.getInstance(getContext()).addToRequestQueue(request); + } + + private void onBlockSuccess(boolean blocked, int position) { + BlocksAdapter blocksAdapter = (BlocksAdapter) adapter; + blocksAdapter.setBlocked(blocked, position); + } + + private void onBlockFailure(boolean block, String id) { + String verb; + if (block) { + verb = "block"; + } else { + verb = "unblock"; + } + Log.e(TAG, String.format("Failed to %s account id %s", verb, id)); + } + private boolean jumpToTopAllowed() { return type != Type.BLOCKS; } diff --git a/app/src/main/java/com/keylesspalace/tusky/BlocksAdapter.java b/app/src/main/java/com/keylesspalace/tusky/BlocksAdapter.java new file mode 100644 index 000000000..c3d233944 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/BlocksAdapter.java @@ -0,0 +1,141 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky 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. + * + * Tusky 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 Tusky. If not, see + * . */ + +package com.keylesspalace.tusky; + +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.TextView; + +import com.android.volley.toolbox.NetworkImageView; + +import java.util.HashSet; +import java.util.Set; + +class BlocksAdapter extends AccountAdapter { + private static final int VIEW_TYPE_BLOCKED_USER = 0; + private static final int VIEW_TYPE_FOOTER = 1; + + private Set unblockedAccountPositions; + + BlocksAdapter(AccountActionListener accountActionListener, + FooterActionListener footerActionListener) { + super(accountActionListener, footerActionListener); + unblockedAccountPositions = new HashSet<>(); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + switch (viewType) { + default: + case VIEW_TYPE_BLOCKED_USER: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_blocked_user, parent, false); + return new BlockedUserViewHolder(view); + } + case VIEW_TYPE_FOOTER: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_footer, parent, false); + return new FooterViewHolder(view); + } + } + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { + if (position < accountList.size()) { + BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder; + holder.setupWithAccount(accountList.get(position)); + boolean blocked = !unblockedAccountPositions.contains(position); + holder.setupActionListener(accountActionListener, blocked, position); + } else { + FooterViewHolder holder = (FooterViewHolder) viewHolder; + holder.setState(footerState); + holder.setupButton(footerActionListener); + holder.setRetryMessage(R.string.footer_retry_accounts); + holder.setEndOfTimelineMessage(R.string.footer_end_of_accounts); + } + } + + @Override + public int getItemViewType(int position) { + if (position == accountList.size()) { + return VIEW_TYPE_FOOTER; + } else { + return VIEW_TYPE_BLOCKED_USER; + } + } + + public void setBlocked(boolean blocked, int position) { + if (blocked) { + unblockedAccountPositions.remove(position); + } else { + unblockedAccountPositions.add(position); + } + notifyItemChanged(position); + } + + private static class BlockedUserViewHolder extends RecyclerView.ViewHolder { + private NetworkImageView avatar; + private TextView username; + private TextView displayName; + private Button unblock; + private String id; + + BlockedUserViewHolder(View itemView) { + super(itemView); + avatar = (NetworkImageView) itemView.findViewById(R.id.blocked_user_avatar); + displayName = (TextView) itemView.findViewById(R.id.blocked_user_display_name); + username = (TextView) itemView.findViewById(R.id.blocked_user_username); + unblock = (Button) itemView.findViewById(R.id.blocked_user_unblock); + } + + void setupWithAccount(Account account) { + id = account.id; + displayName.setText(account.displayName); + String format = username.getContext().getString(R.string.status_username_format); + String formattedUsername = String.format(format, account.username); + username.setText(formattedUsername); + avatar.setImageUrl(account.avatar, + VolleySingleton.getInstance(avatar.getContext()).getImageLoader()); + } + + void setupActionListener(final AccountActionListener listener, final boolean blocked, + final int position) { + int unblockTextId; + if (blocked) { + unblockTextId = R.string.action_unblock; + } else { + unblockTextId = R.string.action_block; + } + unblock.setText(unblock.getContext().getString(unblockTextId)); + unblock.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onBlock(!blocked, id, position); + } + }); + avatar.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onViewAccount(id); + } + }); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/FollowAdapter.java b/app/src/main/java/com/keylesspalace/tusky/FollowAdapter.java new file mode 100644 index 000000000..13b865d05 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/FollowAdapter.java @@ -0,0 +1,119 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is part of Tusky. + * + * Tusky 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. + * + * Tusky 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 Tusky. If not, see + * . */ + +package com.keylesspalace.tusky; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.android.volley.toolbox.ImageLoader; +import com.android.volley.toolbox.NetworkImageView; + +/** Both for follows and following lists. */ +class FollowAdapter extends AccountAdapter { + private static final int VIEW_TYPE_ACCOUNT = 0; + private static final int VIEW_TYPE_FOOTER = 1; + + FollowAdapter(AccountActionListener accountActionListener, + FooterActionListener footerActionListener) { + super(accountActionListener, footerActionListener); + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + switch (viewType) { + default: + case VIEW_TYPE_ACCOUNT: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_account, parent, false); + return new AccountViewHolder(view); + } + case VIEW_TYPE_FOOTER: { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_footer, parent, false); + return new FooterViewHolder(view); + } + } + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) { + if (position < accountList.size()) { + AccountViewHolder holder = (AccountViewHolder) viewHolder; + holder.setupWithAccount(accountList.get(position)); + holder.setupActionListener(accountActionListener); + } else { + FooterViewHolder holder = (FooterViewHolder) viewHolder; + holder.setState(footerState); + holder.setupButton(footerActionListener); + holder.setRetryMessage(R.string.footer_retry_accounts); + holder.setEndOfTimelineMessage(R.string.footer_end_of_accounts); + } + } + + @Override + public int getItemViewType(int position) { + if (position == accountList.size()) { + return VIEW_TYPE_FOOTER; + } else { + return VIEW_TYPE_ACCOUNT; + } + } + + private static class AccountViewHolder extends RecyclerView.ViewHolder { + private View container; + private TextView username; + private TextView displayName; + private TextView note; + private NetworkImageView avatar; + private String id; + + AccountViewHolder(View itemView) { + super(itemView); + container = itemView.findViewById(R.id.account_container); + username = (TextView) itemView.findViewById(R.id.account_username); + displayName = (TextView) itemView.findViewById(R.id.account_display_name); + note = (TextView) itemView.findViewById(R.id.account_note); + avatar = (NetworkImageView) itemView.findViewById(R.id.account_avatar); + avatar.setDefaultImageResId(R.drawable.avatar_default); + avatar.setErrorImageResId(R.drawable.avatar_error); + } + + void setupWithAccount(Account account) { + id = account.id; + String format = username.getContext().getString(R.string.status_username_format); + String formattedUsername = String.format(format, account.username); + username.setText(formattedUsername); + displayName.setText(account.displayName); + note.setText(account.note); + Context context = avatar.getContext(); + ImageLoader imageLoader = VolleySingleton.getInstance(context).getImageLoader(); + avatar.setImageUrl(account.avatar, imageLoader); + } + + void setupActionListener(final AccountActionListener listener) { + container.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + listener.onViewAccount(id); + } + }); + } + } +} diff --git a/app/src/main/res/layout/item_blocked_user.xml b/app/src/main/res/layout/item_blocked_user.xml new file mode 100644 index 000000000..6ee406fee --- /dev/null +++ b/app/src/main/res/layout/item_blocked_user.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + +