mirror of
https://github.com/accelforce/Yuito
synced 2025-02-01 08:26:56 +01:00
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 card
This commit is contained in:
parent
4864bb79d9
commit
c869886c19
@ -264,7 +264,7 @@ class AccountActivity : BottomSheetActivity(), ActionButtonActivity, HasSupportF
|
||||
val accountListClickListener = { v: View ->
|
||||
val type = when (v.id) {
|
||||
R.id.accountFollowers-> AccountListActivity.Type.FOLLOWERS
|
||||
R.id.accountFollowing -> AccountListActivity.Type.FOLLOWING
|
||||
R.id.accountFollowing -> AccountListActivity.Type.FOLLOWS
|
||||
else -> throw AssertionError()
|
||||
}
|
||||
val accountListIntent = AccountListActivity.newIntent(this, type, accountId)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
102
app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt
Normal file
102
app/src/main/java/com/keylesspalace/tusky/AccountListActivity.kt
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -30,7 +30,6 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||
static final int VIEW_TYPE_ACCOUNT = 0;
|
||||
static final int VIEW_TYPE_FOOTER = 1;
|
||||
|
||||
|
||||
List<Account> accountList;
|
||||
AccountActionListener accountActionListener;
|
||||
private boolean bottomLoading;
|
||||
@ -60,7 +59,7 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void addItems(List<Account> newAccounts) {
|
||||
public void addItems(@NonNull List<Account> newAccounts) {
|
||||
int end = accountList.size();
|
||||
Account last = accountList.get(end - 1);
|
||||
if (last != null && !findAccount(newAccounts, last.getId())) {
|
||||
@ -82,7 +81,7 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean findAccount(List<Account> accounts, String id) {
|
||||
private static boolean findAccount(@NonNull List<Account> accounts, String id) {
|
||||
for (Account account : accounts) {
|
||||
if (account.getId().equals(id)) {
|
||||
return true;
|
||||
@ -101,7 +100,7 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||
return account;
|
||||
}
|
||||
|
||||
public void addItem(Account account, int position) {
|
||||
public void addItem(@NonNull Account account, int position) {
|
||||
if (position < 0 || position > accountList.size()) {
|
||||
return;
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import com.keylesspalace.tusky.entity.Card;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.CustomURLSpan;
|
||||
import com.keylesspalace.tusky.util.HtmlUtils;
|
||||
import com.keylesspalace.tusky.util.LinkHelper;
|
||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||
import com.squareup.picasso.Picasso;
|
||||
@ -30,6 +31,7 @@ import java.text.NumberFormat;
|
||||
import java.util.Date;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||
private TextView reblogs;
|
||||
@ -40,6 +42,10 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||
private TextView cardTitle;
|
||||
private TextView cardDescription;
|
||||
private TextView cardUrl;
|
||||
private View infoDivider;
|
||||
private View favReblogInfoContainer;
|
||||
|
||||
private NumberFormat numberFormat = NumberFormat.getNumberInstance();
|
||||
|
||||
StatusDetailedViewHolder(View view) {
|
||||
super(view, false);
|
||||
@ -51,6 +57,8 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||
cardTitle = view.findViewById(R.id.card_title);
|
||||
cardDescription = view.findViewById(R.id.card_description);
|
||||
cardUrl = view.findViewById(R.id.card_link);
|
||||
infoDivider = view.findViewById(R.id.status_info_divider);
|
||||
favReblogInfoContainer = view.findViewById(R.id.status_reblog_fav_info);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -68,6 +76,45 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||
}
|
||||
}
|
||||
|
||||
private void setReblogAndFavCount(int reblogCount, int favCount, StatusActionListener listener) {
|
||||
|
||||
if(reblogCount > 0) {
|
||||
String reblogCountString = numberFormat.format(reblogCount);
|
||||
reblogs.setText(HtmlUtils.fromHtml(reblogs.getResources().getQuantityString(R.plurals.reblogs, reblogCount, reblogCountString)));
|
||||
reblogs.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
reblogs.setVisibility(View.GONE);
|
||||
}
|
||||
if(favCount > 0) {
|
||||
String favCountString = numberFormat.format(favCount);
|
||||
favourites.setText(HtmlUtils.fromHtml(favourites.getResources().getQuantityString(R.plurals.favs, favCount, favCountString)));
|
||||
favourites.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
favourites.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if(reblogs.getVisibility() == View.GONE && favourites.getVisibility() == View.GONE) {
|
||||
infoDivider.setVisibility(View.GONE);
|
||||
favReblogInfoContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
infoDivider.setVisibility(View.VISIBLE);
|
||||
favReblogInfoContainer.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
reblogs.setOnClickListener( v -> {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onShowReblogs(position);
|
||||
}
|
||||
});
|
||||
favourites.setOnClickListener( v -> {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onShowFavs(position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setApplication(@Nullable Status.Application app) {
|
||||
if (app != null) {
|
||||
|
||||
@ -91,12 +138,11 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||
boolean mediaPreviewEnabled) {
|
||||
super.setupWithStatus(status, listener, mediaPreviewEnabled);
|
||||
|
||||
NumberFormat numberFormat = NumberFormat.getNumberInstance();
|
||||
setReblogAndFavCount(status.getReblogsCount(), status.getFavouritesCount(), listener);
|
||||
|
||||
reblogs.setText(numberFormat.format(status.getReblogsCount()));
|
||||
favourites.setText(numberFormat.format(status.getFavouritesCount()));
|
||||
setApplication(status.getApplication());
|
||||
|
||||
|
||||
View.OnLongClickListener longClickListener = view -> {
|
||||
TextView textView = (TextView)view;
|
||||
ClipboardManager clipboard = (ClipboardManager) view.getContext().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -18,6 +18,7 @@ package com.keylesspalace.tusky.fragment;
|
||||
import androidx.arch.core.util.Function;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
@ -37,6 +38,8 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.keylesspalace.tusky.AccountListActivity;
|
||||
import com.keylesspalace.tusky.BaseActivity;
|
||||
import com.keylesspalace.tusky.BuildConfig;
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.ViewThreadActivity;
|
||||
@ -237,7 +240,8 @@ public final class ViewThreadFragment extends SFragment implements
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
||||
if (response.isSuccessful()) {
|
||||
setReblogForStatus(position, status, reblog);
|
||||
updateStatus(position, response.body());
|
||||
|
||||
eventHub.dispatch(new ReblogEvent(status.getId(), reblog));
|
||||
}
|
||||
}
|
||||
@ -250,24 +254,6 @@ public final class ViewThreadFragment extends SFragment implements
|
||||
});
|
||||
}
|
||||
|
||||
private void setReblogForStatus(int position, Status status, boolean reblog) {
|
||||
status.setReblogged(reblog);
|
||||
|
||||
if (status.getReblog() != null) {
|
||||
status.getReblog().setReblogged(reblog);
|
||||
}
|
||||
|
||||
StatusViewData.Concrete viewdata = statuses.getPairedItem(position);
|
||||
|
||||
StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata));
|
||||
viewDataBuilder.setReblogged(reblog);
|
||||
|
||||
StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData();
|
||||
|
||||
statuses.setPairedItem(position, newViewData);
|
||||
adapter.setItem(position, newViewData, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFavourite(final boolean favourite, final int position) {
|
||||
final Status status = statuses.get(position);
|
||||
@ -275,7 +261,8 @@ public final class ViewThreadFragment extends SFragment implements
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
||||
if (response.isSuccessful()) {
|
||||
setFavForStatus(position, status, favourite);
|
||||
updateStatus(position, response.body());
|
||||
|
||||
eventHub.dispatch(new FavoriteEvent(status.getId(), favourite));
|
||||
}
|
||||
}
|
||||
@ -288,22 +275,20 @@ public final class ViewThreadFragment extends SFragment implements
|
||||
});
|
||||
}
|
||||
|
||||
private void setFavForStatus(int position, Status status, boolean favourite) {
|
||||
status.setFavourited(favourite);
|
||||
private void updateStatus(int position, Status status) {
|
||||
if(position >= 0 && position < statuses.size()) {
|
||||
|
||||
statuses.set(position, status);
|
||||
|
||||
if(position == statusIndex && card != null) {
|
||||
StatusViewData.Concrete viewData = new StatusViewData.Builder(statuses.getPairedItem(position))
|
||||
.setCard(card)
|
||||
.createStatusViewData();
|
||||
statuses.setPairedItem(position, viewData);
|
||||
}
|
||||
adapter.setItem(position, statuses.getPairedItem(position), true);
|
||||
|
||||
if (status.getReblog() != null) {
|
||||
status.getReblog().setFavourited(favourite);
|
||||
}
|
||||
|
||||
StatusViewData.Concrete viewdata = statuses.getPairedItem(position);
|
||||
|
||||
StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata));
|
||||
viewDataBuilder.setFavourited(favourite);
|
||||
|
||||
StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData();
|
||||
|
||||
statuses.setPairedItem(position, newViewData);
|
||||
adapter.setItem(position, newViewData, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -355,10 +340,24 @@ public final class ViewThreadFragment extends SFragment implements
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadMore(int pos) {
|
||||
public void onLoadMore(int position) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowReblogs(int position) {
|
||||
String statusId = statuses.get(position).getId();
|
||||
Intent intent = AccountListActivity.newIntent(getContext(), AccountListActivity.Type.REBLOGGED, statusId);
|
||||
((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShowFavs(int position) {
|
||||
String statusId = statuses.get(position).getId();
|
||||
Intent intent = AccountListActivity.newIntent(getContext(), AccountListActivity.Type.FAVOURITED, statusId);
|
||||
((BaseActivity) getActivity()).startActivityWithSlideInAnimation(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentCollapsedChange(boolean isCollapsed, int position) {
|
||||
if (position < 0 || position >= statuses.size()) {
|
||||
@ -615,14 +614,44 @@ public final class ViewThreadFragment extends SFragment implements
|
||||
Pair<Integer, Status> posAndStatus = findStatusAndPos(event.getStatusId());
|
||||
if (posAndStatus == null) return;
|
||||
//noinspection ConstantConditions
|
||||
setFavForStatus(posAndStatus.first, posAndStatus.second, event.getFavourite());
|
||||
boolean favourite = event.getFavourite();
|
||||
posAndStatus.second.setFavourited(favourite);
|
||||
|
||||
if (posAndStatus.second.getReblog() != null) {
|
||||
posAndStatus.second.getReblog().setFavourited(favourite);
|
||||
}
|
||||
|
||||
StatusViewData.Concrete viewdata = statuses.getPairedItem(posAndStatus.first);
|
||||
|
||||
StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata));
|
||||
viewDataBuilder.setFavourited(favourite);
|
||||
|
||||
StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData();
|
||||
|
||||
statuses.setPairedItem(posAndStatus.first, newViewData);
|
||||
adapter.setItem(posAndStatus.first, newViewData, true);
|
||||
}
|
||||
|
||||
private void handleReblogEvent(ReblogEvent event) {
|
||||
Pair<Integer, Status> posAndStatus = findStatusAndPos(event.getStatusId());
|
||||
if (posAndStatus == null) return;
|
||||
//noinspection ConstantConditions
|
||||
setReblogForStatus(posAndStatus.first, posAndStatus.second, event.getReblog());
|
||||
boolean reblog = event.getReblog();
|
||||
posAndStatus.second.setReblogged(reblog);
|
||||
|
||||
if (posAndStatus.second.getReblog() != null) {
|
||||
posAndStatus.second.getReblog().setReblogged(reblog);
|
||||
}
|
||||
|
||||
StatusViewData.Concrete viewdata = statuses.getPairedItem(posAndStatus.first);
|
||||
|
||||
StatusViewData.Builder viewDataBuilder = new StatusViewData.Builder((viewdata));
|
||||
viewDataBuilder.setReblogged(reblog);
|
||||
|
||||
StatusViewData.Concrete newViewData = viewDataBuilder.createStatusViewData();
|
||||
|
||||
statuses.setPairedItem(posAndStatus.first, newViewData);
|
||||
adapter.setItem(posAndStatus.first, newViewData, true);
|
||||
}
|
||||
|
||||
private void handleStatusComposedEvent(StatusComposedEvent event) {
|
||||
|
@ -37,4 +37,17 @@ public interface StatusActionListener extends LinkListener {
|
||||
* @param position The position of the status in the list.
|
||||
*/
|
||||
void onContentCollapsedChange(boolean isCollapsed, int position);
|
||||
|
||||
/**
|
||||
* called when the reblog count has been clicked
|
||||
* @param position The position of the status in the list.
|
||||
*/
|
||||
default void onShowReblogs(int position) {}
|
||||
|
||||
/**
|
||||
* called when the favourite count has been clicked
|
||||
* @param position The position of the status in the list.
|
||||
*/
|
||||
default void onShowFavs(int position) {}
|
||||
|
||||
}
|
||||
|
@ -51,6 +51,10 @@ import retrofit2.http.Part;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
|
||||
/**
|
||||
* for documentation of the Mastodon REST API see https://docs.joinmastodon.org/api/
|
||||
*/
|
||||
public interface MastodonApi {
|
||||
String ENDPOINT_AUTHORIZE = "/oauth/authorize";
|
||||
String DOMAIN_HEADER = "domain";
|
||||
@ -131,16 +135,12 @@ public interface MastodonApi {
|
||||
@GET("api/v1/statuses/{id}/reblogged_by")
|
||||
Call<List<Account>> statusRebloggedBy(
|
||||
@Path("id") String statusId,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
@Query("max_id") String maxId);
|
||||
|
||||
@GET("api/v1/statuses/{id}/favourited_by")
|
||||
Call<List<Account>> statusFavouritedBy(
|
||||
@Path("id") String statusId,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
@Query("max_id") String maxId);
|
||||
|
||||
@DELETE("api/v1/statuses/{id}")
|
||||
Call<ResponseBody> deleteStatus(@Path("id") String statusId);
|
||||
@ -218,16 +218,12 @@ public interface MastodonApi {
|
||||
@GET("api/v1/accounts/{id}/followers")
|
||||
Call<List<Account>> accountFollowers(
|
||||
@Path("id") String accountId,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
@Query("max_id") String maxId);
|
||||
|
||||
@GET("api/v1/accounts/{id}/following")
|
||||
Call<List<Account>> accountFollowing(
|
||||
@Path("id") String accountId,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
@Query("max_id") String maxId);
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST("api/v1/accounts/{id}/follow")
|
||||
@ -252,16 +248,10 @@ public interface MastodonApi {
|
||||
Call<List<Relationship>> relationships(@Query("id[]") List<String> accountIds);
|
||||
|
||||
@GET("api/v1/blocks")
|
||||
Call<List<Account>> blocks(
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
Call<List<Account>> blocks(@Query("max_id") String maxId);
|
||||
|
||||
@GET("api/v1/mutes")
|
||||
Call<List<Account>> mutes(
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
Call<List<Account>> mutes(@Query("max_id") String maxId);
|
||||
|
||||
@GET("api/v1/favourites")
|
||||
Call<List<Status>> favourites(
|
||||
@ -270,10 +260,7 @@ public interface MastodonApi {
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
@GET("api/v1/follow_requests")
|
||||
Call<List<Account>> followRequests(
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
Call<List<Account>> followRequests(@Query("max_id") String maxId);
|
||||
|
||||
@POST("api/v1/follow_requests/{id}/authorize")
|
||||
Call<Relationship> authorizeFollowRequest(@Path("id") String accountId);
|
||||
|
@ -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" />
|
@ -15,8 +15,8 @@
|
||||
android:id="@+id/status_avatar"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="14dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:layout_marginEnd="14dp"
|
||||
android:contentDescription="@string/action_view_profile"
|
||||
android:scaleType="centerCrop"
|
||||
tools:src="@drawable/avatar_default" />
|
||||
@ -25,8 +25,8 @@
|
||||
android:id="@+id/status_name_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_toEndOf="@+id/status_avatar"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="48dp"
|
||||
@ -71,15 +71,15 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/status_content_warning_description"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:background="?attr/content_warning_button"
|
||||
android:minHeight="0dp"
|
||||
android:minWidth="160dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:minHeight="0dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:textAllCaps="true"
|
||||
android:textOff="@string/status_content_warning_show_more"
|
||||
android:textOn="@string/status_content_warning_show_less"
|
||||
@ -118,10 +118,10 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="6dp"
|
||||
android:paddingLeft="6dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingRight="6dp"
|
||||
android:paddingTop="6dp">
|
||||
android:paddingBottom="6dp">
|
||||
|
||||
<!--TODO: check if this needs emoji support-->
|
||||
<androidx.emoji.widget.EmojiTextView
|
||||
@ -164,8 +164,8 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/card_view"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:layout_marginTop="@dimen/status_media_preview_margin_top">
|
||||
android:layout_marginTop="@dimen/status_media_preview_margin_top"
|
||||
android:layout_marginBottom="4dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/status_media_preview_0"
|
||||
@ -308,23 +308,66 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/status_media_preview_container"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:layout_marginTop="10dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:drawablePadding="4dp"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
android:textSize="?attr/status_text_medium" />
|
||||
|
||||
<View
|
||||
android:id="@+id/status_info_divider"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_below="@id/status_timestamp_info"
|
||||
android:background="?attr/status_divider_drawable"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/status_reblog_fav_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/status_timestamp_info"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_reblogs"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:padding="4dp"
|
||||
android:textSize="?attr/status_text_medium" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_favourites"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:padding="4dp"
|
||||
android:textSize="?attr/status_text_medium" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_below="@id/status_reblog_fav_info"
|
||||
android:background="?attr/status_divider_drawable"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/status_reblog_fav_info"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingBottom="4dp"
|
||||
android:paddingTop="4dp">
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/status_reply"
|
||||
@ -353,12 +396,6 @@
|
||||
sparkbutton:primaryColor="@color/tusky_blue"
|
||||
sparkbutton:secondaryColor="@color/tusky_blue_light" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_reblogs"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="?attr/status_text_medium" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
@ -377,12 +414,6 @@
|
||||
sparkbutton:primaryColor="@color/tusky_orange"
|
||||
sparkbutton:secondaryColor="@color/tusky_orange_light" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/status_favourites"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="?attr/status_text_medium" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -360,4 +360,17 @@
|
||||
<string name="unpin_action">Unpin</string>
|
||||
<string name="pin_action">Pin</string>
|
||||
|
||||
<plurals name="favs">
|
||||
<item quantity="one"><b>%1$s</b> Favourite</item>
|
||||
<item quantity="other"><b>%1$s</b> Favourites</item>
|
||||
</plurals>
|
||||
|
||||
<plurals name="reblogs">
|
||||
<item quantity="one"><b>%s</b> Boost</item>
|
||||
<item quantity="other"><b>%s</b> Boosts</item>
|
||||
</plurals>
|
||||
|
||||
<string name="title_reblogged_by">Boosted by</string>
|
||||
<string name="title_favourited_by">Favourited by</string>
|
||||
|
||||
</resources>
|
||||
|
Loading…
x
Reference in New Issue
Block a user