add the ability to see who faved or boosted a toot (#962)
* move reblog/fav count up in detailed status view and make them clickable * use status object returned by api when reblogging/faving * Reblogs -> Boosts * add support for viewing who faved/reblogged a status * add onShowReblogs/onShowFavs to listener, fix display bug * remove unneeded icon from previous revision * small code improvements * fix liking/boosting toot with cardmain
parent
c0f7c4d8c1
commit
82a632eecc
@ -1,149 +0,0 @@ |
||||
/* Copyright 2017 Andrew Dawson |
||||
* |
||||
* This file is a part of Tusky. |
||||
* |
||||
* 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. |
||||
* |
||||
* 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 <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky; |
||||
|
||||
import android.content.Context; |
||||
import android.content.Intent; |
||||
import android.os.Bundle; |
||||
import androidx.annotation.NonNull; |
||||
import androidx.annotation.Nullable; |
||||
import androidx.fragment.app.Fragment; |
||||
import androidx.fragment.app.FragmentTransaction; |
||||
import androidx.appcompat.app.ActionBar; |
||||
import androidx.appcompat.widget.Toolbar; |
||||
import android.view.MenuItem; |
||||
|
||||
import com.keylesspalace.tusky.fragment.AccountListFragment; |
||||
|
||||
import javax.inject.Inject; |
||||
|
||||
import dagger.android.AndroidInjector; |
||||
import dagger.android.DispatchingAndroidInjector; |
||||
import dagger.android.support.HasSupportFragmentInjector; |
||||
|
||||
public final class AccountListActivity extends BaseActivity implements HasSupportFragmentInjector { |
||||
|
||||
@Inject |
||||
public DispatchingAndroidInjector<Fragment> dispatchingAndroidInjector; |
||||
|
||||
private static final String TYPE_EXTRA = "type"; |
||||
private static final String ARG_EXTRA = "arg"; |
||||
|
||||
public static Intent newIntent(@NonNull Context context, @NonNull Type type, |
||||
@Nullable String argument) { |
||||
Intent intent = new Intent(context, AccountListActivity.class); |
||||
intent.putExtra(TYPE_EXTRA, type); |
||||
if (argument != null) { |
||||
intent.putExtra(ARG_EXTRA, argument); |
||||
} |
||||
return intent; |
||||
} |
||||
|
||||
public enum Type { |
||||
BLOCKS, |
||||
MUTES, |
||||
FOLLOW_REQUESTS, |
||||
FOLLOWERS, |
||||
FOLLOWING, |
||||
} |
||||
|
||||
@Override |
||||
protected void onCreate(@Nullable Bundle savedInstanceState) { |
||||
super.onCreate(savedInstanceState); |
||||
setContentView(R.layout.activity_account_list); |
||||
|
||||
Type type; |
||||
Intent intent = getIntent(); |
||||
if (intent != null) { |
||||
type = (Type) intent.getSerializableExtra("type"); |
||||
} else { |
||||
type = Type.BLOCKS; |
||||
} |
||||
|
||||
Toolbar toolbar = findViewById(R.id.toolbar); |
||||
setSupportActionBar(toolbar); |
||||
ActionBar bar = getSupportActionBar(); |
||||
if (bar != null) { |
||||
switch (type) { |
||||
case BLOCKS: { |
||||
bar.setTitle(getString(R.string.title_blocks)); |
||||
break; |
||||
} |
||||
case MUTES: { |
||||
bar.setTitle(getString(R.string.title_mutes)); |
||||
break; |
||||
} |
||||
case FOLLOW_REQUESTS: { |
||||
bar.setTitle(getString(R.string.title_follow_requests)); |
||||
break; |
||||
} |
||||
case FOLLOWERS: |
||||
bar.setTitle(getString(R.string.title_followers)); |
||||
break; |
||||
case FOLLOWING: |
||||
bar.setTitle(getString(R.string.title_follows)); |
||||
} |
||||
bar.setDisplayHomeAsUpEnabled(true); |
||||
bar.setDisplayShowHomeEnabled(true); |
||||
} |
||||
|
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); |
||||
AccountListFragment fragment; |
||||
switch (type) { |
||||
default: |
||||
case BLOCKS: { |
||||
fragment = AccountListFragment.newInstance(AccountListFragment.Type.BLOCKS); |
||||
break; |
||||
} |
||||
case MUTES: { |
||||
fragment = AccountListFragment.newInstance(AccountListFragment.Type.MUTES); |
||||
break; |
||||
} |
||||
case FOLLOWERS: { |
||||
String argument = intent.getStringExtra(ARG_EXTRA); |
||||
fragment = AccountListFragment.newInstance(AccountListFragment.Type.FOLLOWERS, argument); |
||||
break; |
||||
} |
||||
case FOLLOWING: { |
||||
String argument = intent.getStringExtra(ARG_EXTRA); |
||||
fragment = AccountListFragment.newInstance(AccountListFragment.Type.FOLLOWS, argument); |
||||
break; |
||||
} |
||||
case FOLLOW_REQUESTS: { |
||||
fragment = AccountListFragment.newInstance(AccountListFragment.Type.FOLLOW_REQUESTS); |
||||
break; |
||||
} |
||||
} |
||||
fragmentTransaction.replace(R.id.fragment_container, fragment); |
||||
fragmentTransaction.commit(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean onOptionsItemSelected(MenuItem item) { |
||||
switch (item.getItemId()) { |
||||
case android.R.id.home: { |
||||
onBackPressed(); |
||||
return true; |
||||
} |
||||
} |
||||
return super.onOptionsItemSelected(item); |
||||
} |
||||
|
||||
@Override |
||||
public AndroidInjector<Fragment> supportFragmentInjector() { |
||||
return dispatchingAndroidInjector; |
||||
} |
||||
} |
@ -0,0 +1,102 @@ |
||||
/* Copyright 2017 Andrew Dawson |
||||
* |
||||
* This file is a part of Tusky. |
||||
* |
||||
* 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. |
||||
* |
||||
* 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 <http://www.gnu.org/licenses>. */ |
||||
|
||||
package com.keylesspalace.tusky |
||||
|
||||
import android.content.Context |
||||
import android.content.Intent |
||||
import android.os.Bundle |
||||
import androidx.fragment.app.Fragment |
||||
import android.view.MenuItem |
||||
|
||||
import com.keylesspalace.tusky.fragment.AccountListFragment |
||||
|
||||
import javax.inject.Inject |
||||
|
||||
import dagger.android.AndroidInjector |
||||
import dagger.android.DispatchingAndroidInjector |
||||
import dagger.android.support.HasSupportFragmentInjector |
||||
import kotlinx.android.synthetic.main.toolbar_basic.* |
||||
|
||||
class AccountListActivity : BaseActivity(), HasSupportFragmentInjector { |
||||
|
||||
@Inject |
||||
lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Fragment> |
||||
|
||||
enum class Type { |
||||
FOLLOWS, |
||||
FOLLOWERS, |
||||
BLOCKS, |
||||
MUTES, |
||||
FOLLOW_REQUESTS, |
||||
REBLOGGED, |
||||
FAVOURITED |
||||
} |
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
||||
super.onCreate(savedInstanceState) |
||||
setContentView(R.layout.activity_account_list) |
||||
|
||||
val type = intent.getSerializableExtra(EXTRA_TYPE) as Type |
||||
val id: String? = intent.getStringExtra(EXTRA_ID) |
||||
|
||||
setSupportActionBar(toolbar) |
||||
supportActionBar?.apply { |
||||
when (type) { |
||||
AccountListActivity.Type.BLOCKS -> setTitle(R.string.title_blocks) |
||||
AccountListActivity.Type.MUTES -> setTitle(R.string.title_mutes) |
||||
AccountListActivity.Type.FOLLOW_REQUESTS -> setTitle(R.string.title_follow_requests) |
||||
AccountListActivity.Type.FOLLOWERS -> setTitle(R.string.title_followers) |
||||
AccountListActivity.Type.FOLLOWS -> setTitle(R.string.title_follows) |
||||
AccountListActivity.Type.REBLOGGED -> setTitle(R.string.title_reblogged_by) |
||||
AccountListActivity.Type.FAVOURITED -> setTitle(R.string.title_favourited_by) |
||||
} |
||||
setDisplayHomeAsUpEnabled(true) |
||||
setDisplayShowHomeEnabled(true) |
||||
} |
||||
|
||||
supportFragmentManager |
||||
.beginTransaction() |
||||
.replace(R.id.fragment_container, AccountListFragment.newInstance(type, id)) |
||||
.commit() |
||||
} |
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean { |
||||
when (item.itemId) { |
||||
android.R.id.home -> { |
||||
onBackPressed() |
||||
return true |
||||
} |
||||
} |
||||
return super.onOptionsItemSelected(item) |
||||
} |
||||
|
||||
override fun supportFragmentInjector(): AndroidInjector<Fragment>? { |
||||
return dispatchingAndroidInjector |
||||
} |
||||
|
||||
companion object { |
||||
private const val EXTRA_TYPE = "type" |
||||
private const val EXTRA_ID = "id" |
||||
|
||||
@JvmStatic |
||||
fun newIntent(context: Context, type: Type, id: String? = null): Intent { |
||||
return Intent(context, AccountListActivity::class.java).apply { |
||||
putExtra(EXTRA_TYPE, type) |
||||
putExtra(EXTRA_ID, id) |
||||
} |
||||
} |
||||
} |
||||
} |
@ -1,404 +0,0 @@ |
||||
/* Copyright 2017 Andrew Dawson |
||||
* |
||||
* This file is a part of Tusky. |
||||
* |
||||
* 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. |
||||
* |
||||
* 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 <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.fragment; |
||||
|
||||
import android.content.Context; |
||||
import android.content.Intent; |
||||
import android.graphics.drawable.Drawable; |
||||
import android.os.Bundle; |
||||
import androidx.annotation.NonNull; |
||||
import androidx.annotation.Nullable; |
||||
import com.google.android.material.snackbar.Snackbar; |
||||
import androidx.recyclerview.widget.DividerItemDecoration; |
||||
import androidx.recyclerview.widget.LinearLayoutManager; |
||||
import androidx.recyclerview.widget.RecyclerView; |
||||
import android.util.Log; |
||||
import android.view.LayoutInflater; |
||||
import android.view.View; |
||||
import android.view.ViewGroup; |
||||
|
||||
import com.keylesspalace.tusky.AccountActivity; |
||||
import com.keylesspalace.tusky.R; |
||||
import com.keylesspalace.tusky.adapter.AccountAdapter; |
||||
import com.keylesspalace.tusky.adapter.BlocksAdapter; |
||||
import com.keylesspalace.tusky.adapter.FollowAdapter; |
||||
import com.keylesspalace.tusky.adapter.FollowRequestsAdapter; |
||||
import com.keylesspalace.tusky.adapter.MutesAdapter; |
||||
import com.keylesspalace.tusky.di.Injectable; |
||||
import com.keylesspalace.tusky.entity.Account; |
||||
import com.keylesspalace.tusky.entity.Relationship; |
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener; |
||||
import com.keylesspalace.tusky.network.MastodonApi; |
||||
import com.keylesspalace.tusky.util.HttpHeaderLink; |
||||
import com.keylesspalace.tusky.util.ThemeUtils; |
||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener; |
||||
|
||||
import java.util.List; |
||||
|
||||
import javax.inject.Inject; |
||||
|
||||
import retrofit2.Call; |
||||
import retrofit2.Callback; |
||||
import retrofit2.Response; |
||||
|
||||
public class AccountListFragment extends BaseFragment implements AccountActionListener, |
||||
Injectable { |
||||
private static final String TAG = "AccountList"; // logging tag
|
||||
|
||||
public AccountListFragment() { |
||||
} |
||||
|
||||
public enum Type { |
||||
FOLLOWS, |
||||
FOLLOWERS, |
||||
BLOCKS, |
||||
MUTES, |
||||
FOLLOW_REQUESTS, |
||||
} |
||||
|
||||
@Inject |
||||
public MastodonApi api; |
||||
|
||||
private Type type; |
||||
private String accountId; |
||||
private LinearLayoutManager layoutManager; |
||||
private RecyclerView recyclerView; |
||||
private EndlessOnScrollListener scrollListener; |
||||
private AccountAdapter adapter; |
||||
private boolean fetching = false; |
||||
private String bottomId; |
||||
|
||||
public static AccountListFragment newInstance(Type type) { |
||||
Bundle arguments = new Bundle(); |
||||
AccountListFragment fragment = new AccountListFragment(); |
||||
arguments.putSerializable("type", type); |
||||
fragment.setArguments(arguments); |
||||
return fragment; |
||||
} |
||||
|
||||
public static AccountListFragment newInstance(Type type, String accountId) { |
||||
Bundle arguments = new Bundle(); |
||||
AccountListFragment fragment = new AccountListFragment(); |
||||
arguments.putSerializable("type", type); |
||||
arguments.putString("accountId", accountId); |
||||
fragment.setArguments(arguments); |
||||
return fragment; |
||||
} |
||||
|
||||
@Override |
||||
public void onCreate(@Nullable Bundle savedInstanceState) { |
||||
super.onCreate(savedInstanceState); |
||||
Bundle arguments = getArguments(); |
||||
type = (Type) arguments.getSerializable("type"); |
||||
accountId = arguments.getString("accountId"); |
||||
api = null; |
||||
} |
||||
|
||||
@Nullable |
||||
@Override |
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, |
||||
@Nullable Bundle savedInstanceState) { |
||||
|
||||
View rootView = inflater.inflate(R.layout.fragment_account_list, container, false); |
||||
|
||||
Context context = getContext(); |
||||
recyclerView = rootView.findViewById(R.id.recycler_view); |
||||
recyclerView.setHasFixedSize(true); |
||||
layoutManager = new LinearLayoutManager(context); |
||||
recyclerView.setLayoutManager(layoutManager); |
||||
DividerItemDecoration divider = new DividerItemDecoration( |
||||
context, layoutManager.getOrientation()); |
||||
Drawable drawable = ThemeUtils.getDrawable(context, R.attr.status_divider_drawable, |
||||
R.drawable.status_divider_dark); |
||||
divider.setDrawable(drawable); |
||||
recyclerView.addItemDecoration(divider); |
||||
scrollListener = null; |
||||
if (type == Type.BLOCKS) { |
||||
adapter = new BlocksAdapter(this); |
||||
} else if (type == Type.MUTES) { |
||||
adapter = new MutesAdapter(this); |
||||
} else if (type == Type.FOLLOW_REQUESTS) { |
||||
adapter = new FollowRequestsAdapter(this); |
||||
} else { |
||||
adapter = new FollowAdapter(this); |
||||
} |
||||
recyclerView.setAdapter(adapter); |
||||
|
||||
|
||||
return rootView; |
||||
} |
||||
|
||||
@Override |
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) { |
||||
super.onActivityCreated(savedInstanceState); |
||||
// Just use the basic scroll listener to load more accounts.
|
||||
scrollListener = new EndlessOnScrollListener(layoutManager) { |
||||
@Override |
||||
public void onLoadMore(int totalItemsCount, RecyclerView view) { |
||||
AccountListFragment.this.onLoadMore(); |
||||
} |
||||
}; |
||||
|
||||
recyclerView.addOnScrollListener(scrollListener); |
||||
|
||||
fetchAccounts(null); |
||||
|
||||
} |
||||
|
||||
@Override |
||||
public void onViewAccount(String id) { |
||||
Context context = getContext(); |
||||
if(context != null) { |
||||
Intent intent = AccountActivity.getIntent(context, id); |
||||
startActivity(intent); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onMute(final boolean mute, final String id, final int position) { |
||||
Callback<Relationship> callback = new Callback<Relationship>() { |
||||
@Override |
||||
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) { |
||||
if (response.isSuccessful()) { |
||||
onMuteSuccess(mute, id, position); |
||||
} else { |
||||
onMuteFailure(mute, id); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) { |
||||
onMuteFailure(mute, id); |
||||
} |
||||
}; |
||||
|
||||
Call<Relationship> call; |
||||
if (!mute) { |
||||
call = api.unmuteAccount(id); |
||||
} else { |
||||
call = api.muteAccount(id); |
||||
} |
||||
callList.add(call); |
||||
call.enqueue(callback); |
||||
} |
||||
|
||||
private void onMuteSuccess(boolean muted, final String id, final int position) { |
||||
if (muted) { |
||||
return; |
||||
} |
||||
final MutesAdapter mutesAdapter = (MutesAdapter) adapter; |
||||
final Account unmutedUser = mutesAdapter.removeItem(position); |
||||
View.OnClickListener listener = v -> { |
||||
mutesAdapter.addItem(unmutedUser, position); |
||||
onMute(true, id, position); |
||||
}; |
||||
Snackbar.make(recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG) |
||||
.setAction(R.string.action_undo, listener) |
||||
.show(); |
||||
} |
||||
|
||||
private void onMuteFailure(boolean mute, String id) { |
||||
String verb; |
||||
if (mute) { |
||||
verb = "mute"; |
||||
} else { |
||||
verb = "unmute"; |
||||
} |
||||
Log.e(TAG, String.format("Failed to %s account id %s", verb, id)); |
||||
} |
||||
|
||||
@Override |
||||
public void onBlock(final boolean block, final String id, final int position) { |
||||
Callback<Relationship> cb = new Callback<Relationship>() { |
||||
@Override |
||||
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) { |
||||
if (response.isSuccessful()) { |
||||
onBlockSuccess(block, id, position); |
||||
} else { |
||||
onBlockFailure(block, id); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) { |
||||
onBlockFailure(block, id); |
||||
} |
||||
}; |
||||
|
||||
Call<Relationship> call; |
||||
if (!block) { |
||||
call = api.unblockAccount(id); |
||||
} else { |
||||
call = api.blockAccount(id); |
||||
} |
||||
callList.add(call); |
||||
call.enqueue(cb); |
||||
} |
||||
|
||||
private void onBlockSuccess(boolean blocked, final String id, final int position) { |
||||
if (blocked) { |
||||
return; |
||||
} |
||||
final BlocksAdapter blocksAdapter = (BlocksAdapter) adapter; |
||||
final Account unblockedUser = blocksAdapter.removeItem(position); |
||||
View.OnClickListener listener = v -> { |
||||
blocksAdapter.addItem(unblockedUser, position); |
||||
onBlock(true, id, position); |
||||
}; |
||||
Snackbar.make(recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG) |
||||
.setAction(R.string.action_undo, listener) |
||||
.show(); |
||||
} |
||||
|
||||
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)); |
||||
} |
||||
|
||||
@Override |
||||
public void onRespondToFollowRequest(final boolean accept, final String accountId, |
||||
final int position) { |
||||
|
||||
Callback<Relationship> callback = new Callback<Relationship>() { |
||||
@Override |
||||
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) { |
||||
if (response.isSuccessful()) { |
||||
onRespondToFollowRequestSuccess(position); |
||||
} else { |
||||
onRespondToFollowRequestFailure(accept, accountId); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onFailure(@NonNull Call<Relationship> call, @NonNull Throwable t) { |
||||
onRespondToFollowRequestFailure(accept, accountId); |
||||
} |
||||
}; |
||||
|
||||
Call<Relationship> call; |
||||
if (accept) { |
||||
call = api.authorizeFollowRequest(accountId); |
||||
} else { |
||||
call = api.rejectFollowRequest(accountId); |
||||
} |
||||
callList.add(call); |
||||
call.enqueue(callback); |
||||
} |
||||
|
||||
private void onRespondToFollowRequestSuccess(int position) { |
||||
FollowRequestsAdapter followRequestsAdapter = (FollowRequestsAdapter) adapter; |
||||
followRequestsAdapter.removeItem(position); |
||||
} |
||||
|
||||
private void onRespondToFollowRequestFailure(boolean accept, String accountId) { |
||||
String verb; |
||||
if (accept) { |
||||
verb = "accept"; |
||||
} else { |
||||
verb = "reject"; |
||||
} |
||||
String message = String.format("Failed to %s account id %s.", verb, accountId); |
||||
Log.e(TAG, message); |
||||
} |
||||
|
||||
private Call<List<Account>> getFetchCallByListType(Type type, String fromId) { |
||||
switch (type) { |
||||
default: |
||||
case FOLLOWS: |
||||
return api.accountFollowing(accountId, fromId, null, null); |
||||
case FOLLOWERS: |
||||
return api.accountFollowers(accountId, fromId, null, null); |
||||
case BLOCKS: |
||||
return api.blocks(fromId, null, null); |
||||
case MUTES: |
||||
return api.mutes(fromId, null, null); |
||||
case FOLLOW_REQUESTS: |
||||
return api.followRequests(fromId, null, null); |
||||
} |
||||
} |
||||
|
||||
private void fetchAccounts(String id) { |
||||
if (fetching) { |
||||
return; |
||||
} |
||||
fetching = true; |
||||
|
||||
if (id != null) { |
||||
recyclerView.post(() -> adapter.setBottomLoading(true)); |
||||
} |
||||
|
||||
Callback<List<Account>> cb = new Callback<List<Account>>() { |
||||
@Override |
||||
public void onResponse(@NonNull Call<List<Account>> call, @NonNull Response<List<Account>> response) { |
||||
if (response.isSuccessful()) { |
||||
String linkHeader = response.headers().get("Link"); |
||||
onFetchAccountsSuccess(response.body(), linkHeader); |
||||
} else { |
||||
onFetchAccountsFailure(new Exception(response.message())); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onFailure(@NonNull Call<List<Account>> call, @NonNull Throwable t) { |
||||
onFetchAccountsFailure((Exception) t); |
||||
} |
||||
}; |
||||
Call<List<Account>> listCall = getFetchCallByListType(type, id); |
||||
callList.add(listCall); |
||||
listCall.enqueue(cb); |
||||
} |
||||
|
||||
private void onFetchAccountsSuccess(List<Account> accounts, String linkHeader) { |
||||
adapter.setBottomLoading(false); |
||||
|
||||
|
||||
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader); |
||||
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next"); |
||||
String fromId = null; |
||||
if (next != null) { |
||||
fromId = next.uri.getQueryParameter("max_id"); |
||||
} |
||||
if (adapter.getItemCount() > 1) { |
||||
adapter.addItems(accounts); |
||||
} else { |
||||
adapter.update(accounts); |
||||
} |
||||
|
||||
bottomId = fromId; |
||||
|
||||
fetching = false; |
||||
|
||||
adapter.setBottomLoading(false); |
||||
} |
||||
|
||||
private void onFetchAccountsFailure(Exception exception) { |
||||
fetching = false; |
||||
Log.e(TAG, "Fetch failure: " + exception.getMessage()); |
||||
} |
||||
|
||||
private void onLoadMore() { |
||||
if(bottomId == null) { |
||||
return; |
||||
} |
||||
fetchAccounts(bottomId); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,336 @@ |
||||
/* Copyright 2017 Andrew Dawson |
||||
* |
||||
* This file is a part of Tusky. |
||||
* |
||||
* 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. |
||||
* |
||||
* 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 <http://www.gnu.org/licenses>. */ |
||||
|
||||
package com.keylesspalace.tusky.fragment |
||||
|
||||
import android.os.Bundle |
||||
import com.google.android.material.snackbar.Snackbar |
||||
import androidx.recyclerview.widget.DividerItemDecoration |
||||
import androidx.recyclerview.widget.LinearLayoutManager |
||||
import androidx.recyclerview.widget.RecyclerView |
||||
import android.util.Log |
||||
import android.view.LayoutInflater |
||||
import android.view.View |
||||
import android.view.ViewGroup |
||||
|
||||
import com.keylesspalace.tusky.AccountActivity |
||||
import com.keylesspalace.tusky.AccountListActivity.Type |
||||
import com.keylesspalace.tusky.BaseActivity |
||||
import com.keylesspalace.tusky.R |
||||
import com.keylesspalace.tusky.adapter.AccountAdapter |
||||
import com.keylesspalace.tusky.adapter.BlocksAdapter |
||||
import com.keylesspalace.tusky.adapter.FollowAdapter |
||||
import com.keylesspalace.tusky.adapter.FollowRequestsAdapter |
||||
import com.keylesspalace.tusky.adapter.MutesAdapter |
||||
import com.keylesspalace.tusky.di.Injectable |
||||
import com.keylesspalace.tusky.entity.Account |
||||
import com.keylesspalace.tusky.entity.Relationship |
||||
import com.keylesspalace.tusky.interfaces.AccountActionListener |
||||
import com.keylesspalace.tusky.network.MastodonApi |
||||
import com.keylesspalace.tusky.util.HttpHeaderLink |
||||
import com.keylesspalace.tusky.util.ThemeUtils |
||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener |
||||
import kotlinx.android.synthetic.main.fragment_account_list.* |
||||
|
||||
import javax.inject.Inject |
||||
|
||||
import retrofit2.Call |
||||
import retrofit2.Callback |
||||
import retrofit2.Response |
||||
|
||||
class AccountListFragment : BaseFragment(), AccountActionListener, Injectable { |
||||
|
||||
@Inject |
||||
lateinit var api: MastodonApi |
||||
|
||||
private lateinit var type: Type |
||||
private var id: String? = null |
||||
private lateinit var scrollListener: EndlessOnScrollListener |
||||
private lateinit var adapter: AccountAdapter |
||||
private var fetching = false |
||||
private var bottomId: String? = null |
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) { |
||||
super.onCreate(savedInstanceState) |
||||
type = arguments?.getSerializable(ARG_TYPE) as Type |
||||
id = arguments?.getString(ARG_ID) |
||||
} |
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { |
||||
return inflater.inflate(R.layout.fragment_account_list, container, false) |
||||
} |
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
||||
super.onViewCreated(view, savedInstanceState) |
||||
|
||||
recyclerView.setHasFixedSize(true) |
||||
val layoutManager = LinearLayoutManager(context) |
||||
recyclerView.layoutManager = layoutManager |
||||
val divider = DividerItemDecoration(context, layoutManager.orientation) |
||||
val drawable = ThemeUtils.getDrawable(context, R.attr.status_divider_drawable, R.drawable.status_divider_dark) |
||||
divider.setDrawable(drawable) |
||||
recyclerView.addItemDecoration(divider) |
||||
|
||||
adapter = when(type) { |
||||
Type.BLOCKS -> BlocksAdapter(this) |
||||
Type.MUTES -> MutesAdapter(this) |
||||
Type.FOLLOW_REQUESTS -> FollowRequestsAdapter(this) |
||||
else -> FollowAdapter(this) |
||||
} |
||||
recyclerView.adapter = adapter |
||||
|
||||
scrollListener = object : EndlessOnScrollListener(layoutManager) { |
||||
override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { |
||||
if (bottomId == null) { |
||||
return |
||||
} |
||||
fetchAccounts(bottomId) |
||||
} |
||||
} |
||||
|
||||
recyclerView.addOnScrollListener(scrollListener) |
||||
|
||||
fetchAccounts() |
||||
} |
||||
|
||||
override fun onViewAccount(id: String) { |
||||
(activity as BaseActivity?)?.let { |
||||
val intent = AccountActivity.getIntent(it, id) |
||||
it.startActivityWithSlideInAnimation(intent) |
||||
} |
||||
} |
||||
|
||||
override fun onMute(mute: Boolean, id: String, position: Int) { |
||||
val callback = object : Callback<Relationship> { |
||||
override fun onResponse(call: Call<Relationship>, response: Response<Relationship>) { |
||||
if (response.isSuccessful) { |
||||
onMuteSuccess(mute, id, position) |
||||
} else { |
||||
onMuteFailure(mute, id) |
||||
} |
||||
} |
||||
|
||||
override fun onFailure(call: Call<Relationship>, t: Throwable) { |
||||
onMuteFailure(mute, id) |
||||
} |
||||
} |
||||
|
||||
val call = if (!mute) { |
||||
api.unmuteAccount(id) |
||||
} else { |
||||
api.muteAccount(id) |
||||
} |
||||
callList.add(call) |
||||
call.enqueue(callback) |
||||
} |
||||
|
||||
private fun onMuteSuccess(muted: Boolean, id: String, position: Int) { |
||||
if (muted) { |
||||
return |
||||
} |
||||
val mutesAdapter = adapter as MutesAdapter |
||||
val unmutedUser = mutesAdapter.removeItem(position) |
||||
|
||||
if(unmutedUser != null) { |
||||
Snackbar.make(recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG) |
||||
.setAction(R.string.action_undo) { |
||||
mutesAdapter.addItem(unmutedUser, position) |
||||
onMute(true, id, position) |
||||
} |
||||
.show() |
||||
} |
||||
} |
||||
|
||||
private fun onMuteFailure(mute: Boolean, accountId: String) { |
||||
val verb = if (mute) { |
||||
"mute" |
||||
} else { |
||||
"unmute" |
||||
} |
||||
Log.e(TAG, "Failed to $verb account id $accountId") |
||||
} |
||||
|
||||
override fun onBlock(block: Boolean, id: String, position: Int) { |
||||
val cb = object : Callback<Relationship> { |
||||
override fun onResponse(call: Call<Relationship>, response: Response<Relationship>) { |
||||
if (response.isSuccessful) { |
||||
onBlockSuccess(block, id, position) |
||||
} else { |
||||
onBlockFailure(block, id) |
||||
} |
||||
} |
||||
|
||||
override fun onFailure(call: Call<Relationship>, t: Throwable) { |
||||
onBlockFailure(block, id) |
||||
} |
||||
} |
||||
|
||||
val call = if (!block) { |
||||
api.unblockAccount(id) |
||||
} else { |
||||
api.blockAccount(id) |
||||
} |
||||
callList.add(call) |
||||
call.enqueue(cb) |
||||
} |
||||
|
||||
private fun onBlockSuccess(blocked: Boolean, id: String, position: Int) { |
||||
if (blocked) { |
||||
return |
||||
} |
||||
val blocksAdapter = adapter as BlocksAdapter |
||||
val unblockedUser = blocksAdapter.removeItem(position) |
||||
|
||||
if(unblockedUser != null) { |
||||
Snackbar.make(recyclerView, R.string.confirmation_unblocked, Snackbar.LENGTH_LONG) |
||||
.setAction(R.string.action_undo) { |
||||
blocksAdapter.addItem(unblockedUser, position) |
||||
onBlock(true, id, position) |
||||
} |
||||
.show() |
||||
} |
||||
} |
||||
|
||||
private fun onBlockFailure(block: Boolean, accountId: String) { |
||||
val verb = if (block) { |
||||
"block" |
||||
} else { |
||||
"unblock" |
||||
} |
||||
Log.e(TAG, "Failed to $verb account accountId $accountId") |
||||
} |
||||
|
||||
override fun onRespondToFollowRequest(accept: Boolean, accountId: String, |
||||
position: Int) { |
||||
|
||||
val callback = object : Callback<Relationship> { |
||||
override fun onResponse(call: Call<Relationship>, response: Response<Relationship>) { |
||||
if (response.isSuccessful) { |
||||
onRespondToFollowRequestSuccess(position) |
||||
} else { |
||||
onRespondToFollowRequestFailure(accept, accountId) |
||||
} |
||||
} |
||||
|
||||
override fun onFailure(call: Call<Relationship>, t: Throwable) { |
||||
onRespondToFollowRequestFailure(accept, accountId) |
||||
} |
||||
} |
||||
|
||||
val call = if (accept) { |
||||
api.authorizeFollowRequest(accountId) |
||||
} else { |
||||
api.rejectFollowRequest(accountId) |
||||
} |
||||
callList.add(call) |
||||
call.enqueue(callback) |
||||
} |
||||
|
||||
private fun onRespondToFollowRequestSuccess(position: Int) { |
||||
val followRequestsAdapter = adapter as FollowRequestsAdapter |
||||
followRequestsAdapter.removeItem(position) |
||||
} |
||||
|
||||
private fun onRespondToFollowRequestFailure(accept: Boolean, accountId: String) { |
||||
val verb = if (accept) { |
||||
"accept" |
||||
} else { |
||||
"reject" |
||||
} |
||||
Log.e(TAG, "Failed to $verb account id $accountId.") |
||||
} |
||||
|
||||
private fun getFetchCallByListType(type: Type, fromId: String?): Call<List<Account>> { |
||||
return when (type) { |
||||
Type.FOLLOWS -> api.accountFollowing(id, fromId) |
||||
Type.FOLLOWERS -> api.accountFollowers(id, fromId) |
||||
Type.BLOCKS -> api.blocks(fromId) |
||||
Type.MUTES -> api.mutes(fromId) |
||||
Type.FOLLOW_REQUESTS -> api.followRequests(fromId) |
||||
Type.REBLOGGED -> api.statusRebloggedBy(id, fromId) |
||||
Type.FAVOURITED -> api.statusFavouritedBy(id, fromId) |
||||
} |
||||
} |
||||
|
||||
private fun fetchAccounts(id: String? = null) { |
||||
if (fetching) { |
||||
return |
||||
} |
||||
fetching = true |
||||
|
||||
if (id != null) { |
||||
recyclerView.post { adapter.setBottomLoading(true) } |
||||
} |
||||
|
||||
val cb = object : Callback<List<Account>> { |
||||
override fun onResponse(call: Call<List<Account>>, response: Response<List<Account>>) { |
||||
val accountList = response.body() |
||||
if (response.isSuccessful && accountList != null) { |
||||
val linkHeader = response.headers().get("Link") |
||||
onFetchAccountsSuccess(accountList, linkHeader) |
||||
} else { |
||||
onFetchAccountsFailure(Exception(response.message())) |
||||
} |
||||
} |
||||
|
||||
override fun onFailure(call: Call<List<Account>>, t: Throwable) { |
||||
onFetchAccountsFailure(t as Exception) |
||||
} |
||||
} |
||||
val listCall = getFetchCallByListType(type, id) |
||||
callList.add(listCall) |
||||
listCall.enqueue(cb) |
||||
} |
||||
|
||||
private fun onFetchAccountsSuccess(accounts: List<Account>, linkHeader: String?) { |
||||
adapter.setBottomLoading(false) |
||||
|
||||
val links = HttpHeaderLink.parse(linkHeader) |
||||
val next = HttpHeaderLink.findByRelationType(links, "next") |
||||
val fromId = next?.uri?.getQueryParameter("max_id") |
||||
|
||||
if (adapter.itemCount > 0) { |
||||
adapter.addItems(accounts) |
||||
} else { |
||||
adapter.update(accounts) |
||||
} |
||||
|
||||
bottomId = fromId |
||||
|
||||
fetching = false |
||||
|
||||
} |
||||
|
||||
private fun onFetchAccountsFailure(exception: Exception) { |
||||
fetching = false |
||||
Log.e(TAG, "Fetch failure", exception) |
||||
} |
||||
|
||||
companion object { |
||||
private const val TAG = "AccountList" // logging tag |
||||
private const val ARG_TYPE = "type" |
||||
private const val ARG_ID = "id" |
||||
|
||||
fun newInstance(type: Type, id: String? = null): AccountListFragment { |
||||
return AccountListFragment().apply { |
||||
arguments = Bundle(2).apply { |
||||
putSerializable(ARG_TYPE, type) |
||||
putString(ARG_ID, id) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
} |
@ -1,6 +1,6 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<androidx.recyclerview.widget.RecyclerView |
||||
xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:id="@+id/recycler_view" |
||||
android:id="@+id/recyclerView" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent" /> |
Loading…
Reference in new issue