Merge branch 'master' into issue_133
This commit is contained in:
commit
ddf654f777
100 changed files with 4337 additions and 906 deletions
|
@ -17,5 +17,7 @@ package com.keylesspalace.tusky;
|
|||
|
||||
interface AccountActionListener {
|
||||
void onViewAccount(String id);
|
||||
void onMute(final boolean mute, final String id, final int position);
|
||||
void onBlock(final boolean block, final String id, final int position);
|
||||
void onRespondToFollowRequest(final boolean accept, final String id, final int position);
|
||||
}
|
||||
|
|
|
@ -28,11 +28,11 @@ import android.support.design.widget.CollapsingToolbarLayout;
|
|||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
|
@ -54,7 +54,7 @@ import retrofit2.Call;
|
|||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class AccountActivity extends BaseActivity {
|
||||
public class AccountActivity extends BaseActivity implements SFragment.OnUserRemovedListener {
|
||||
private static final String TAG = "AccountActivity"; // logging tag
|
||||
|
||||
private String accountId;
|
||||
|
@ -63,6 +63,7 @@ public class AccountActivity extends BaseActivity {
|
|||
private boolean muting = false;
|
||||
private boolean isSelf;
|
||||
private TabLayout tabLayout;
|
||||
private AccountPagerAdapter pagerAdapter;
|
||||
private Account loadedAccount;
|
||||
|
||||
@BindView(R.id.account_locked) ImageView accountLockedView;
|
||||
|
@ -80,8 +81,7 @@ public class AccountActivity extends BaseActivity {
|
|||
accountId = intent.getStringExtra("id");
|
||||
}
|
||||
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
SharedPreferences preferences = getPrivatePreferences();
|
||||
String loggedInAccountId = preferences.getString("loggedInAccountId", null);
|
||||
|
||||
final Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
|
@ -142,6 +142,7 @@ public class AccountActivity extends BaseActivity {
|
|||
// Setup the tabs and timeline pager.
|
||||
AccountPagerAdapter adapter = new AccountPagerAdapter(getSupportFragmentManager(), this,
|
||||
accountId);
|
||||
pagerAdapter = adapter;
|
||||
String[] pageTitles = {
|
||||
getString(R.string.title_statuses),
|
||||
getString(R.string.title_follows),
|
||||
|
@ -165,6 +166,12 @@ public class AccountActivity extends BaseActivity {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
outState.putString("accountId", accountId);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
private void obtainAccount() {
|
||||
mastodonAPI.account(accountId).enqueue(new Callback<Account>() {
|
||||
@Override
|
||||
|
@ -183,12 +190,6 @@ public class AccountActivity extends BaseActivity {
|
|||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
outState.putString("accountId", accountId);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
private void onObtainAccountSuccess(Account account) {
|
||||
loadedAccount = account;
|
||||
|
||||
|
@ -204,9 +205,21 @@ public class AccountActivity extends BaseActivity {
|
|||
|
||||
displayName.setText(account.getDisplayName());
|
||||
|
||||
note.setText(account.note);
|
||||
note.setLinksClickable(true);
|
||||
note.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
LinkHelper.setClickableText(note, account.note, null, new LinkListener() {
|
||||
@Override
|
||||
public void onViewTag(String tag) {
|
||||
Intent intent = new Intent(AccountActivity.this, ViewTagActivity.class);
|
||||
intent.putExtra("hashtag", tag);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAccount(String id) {
|
||||
Intent intent = new Intent(AccountActivity.this, AccountActivity.class);
|
||||
intent.putExtra("id", id);
|
||||
startActivity(intent);
|
||||
}
|
||||
});
|
||||
|
||||
if (account.locked) {
|
||||
accountLockedView.setVisibility(View.VISIBLE);
|
||||
|
@ -289,6 +302,16 @@ public class AccountActivity extends BaseActivity {
|
|||
updateButtons();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserRemoved(String accountId) {
|
||||
for (Fragment fragment : pagerAdapter.getRegisteredFragments()) {
|
||||
if (fragment instanceof StatusRemoveListener) {
|
||||
StatusRemoveListener listener = (StatusRemoveListener) fragment;
|
||||
listener.removePostsByUser(accountId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateFollowButton(FloatingActionButton button) {
|
||||
if (following) {
|
||||
button.setImageResource(R.drawable.ic_person_minus_24px);
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
|
@ -64,6 +65,24 @@ abstract class AccountAdapter extends RecyclerView.Adapter {
|
|||
notifyItemRangeInserted(end, newAccounts.size());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Account removeItem(int position) {
|
||||
if (position < 0 || position >= accountList.size()) {
|
||||
return null;
|
||||
}
|
||||
Account account = accountList.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
return account;
|
||||
}
|
||||
|
||||
void addItem(Account account, int position) {
|
||||
if (position < 0 || position > accountList.size()) {
|
||||
return;
|
||||
}
|
||||
accountList.add(position, account);
|
||||
notifyItemInserted(position);
|
||||
}
|
||||
|
||||
public Account getItem(int position) {
|
||||
if (position >= 0 && position < accountList.size()) {
|
||||
return accountList.get(position);
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
|
@ -23,23 +24,54 @@ import android.support.v7.app.ActionBar;
|
|||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuItem;
|
||||
|
||||
public class BlocksActivity extends BaseActivity {
|
||||
public class AccountListActivity extends BaseActivity {
|
||||
enum Type {
|
||||
BLOCKS,
|
||||
MUTES,
|
||||
FOLLOW_REQUESTS,
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_blocks);
|
||||
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 = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
ActionBar bar = getSupportActionBar();
|
||||
if (bar != null) {
|
||||
bar.setTitle(getString(R.string.title_blocks));
|
||||
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;
|
||||
}
|
||||
}
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
bar.setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
Fragment fragment = AccountFragment.newInstance(AccountFragment.Type.BLOCKS);
|
||||
AccountListFragment.Type fragmentType;
|
||||
switch (type) {
|
||||
default:
|
||||
case BLOCKS: { fragmentType = AccountListFragment.Type.BLOCKS; break; }
|
||||
case MUTES: { fragmentType = AccountListFragment.Type.MUTES; break; }
|
||||
case FOLLOW_REQUESTS: {
|
||||
fragmentType = AccountListFragment.Type.FOLLOW_REQUESTS;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Fragment fragment = AccountListFragment.newInstance(fragmentType);
|
||||
fragmentTransaction.add(R.id.fragment_container, fragment);
|
||||
fragmentTransaction.commit();
|
||||
}
|
|
@ -20,6 +20,7 @@ import android.content.Intent;
|
|||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v7.widget.DividerItemDecoration;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
|
@ -37,16 +38,15 @@ import retrofit2.Call;
|
|||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class AccountFragment extends BaseFragment implements AccountActionListener {
|
||||
private static final String TAG = "Account"; // logging tag
|
||||
|
||||
private Call<List<Account>> listCall;
|
||||
public class AccountListFragment extends BaseFragment implements AccountActionListener {
|
||||
private static final String TAG = "AccountList"; // logging tag
|
||||
|
||||
public enum Type {
|
||||
FOLLOWS,
|
||||
FOLLOWERS,
|
||||
BLOCKS,
|
||||
MUTES,
|
||||
FOLLOW_REQUESTS,
|
||||
}
|
||||
|
||||
private Type type;
|
||||
|
@ -58,18 +58,18 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
|
|||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||
private MastodonAPI api;
|
||||
|
||||
public static AccountFragment newInstance(Type type) {
|
||||
public static AccountListFragment newInstance(Type type) {
|
||||
Bundle arguments = new Bundle();
|
||||
AccountFragment fragment = new AccountFragment();
|
||||
arguments.putString("type", type.name());
|
||||
AccountListFragment fragment = new AccountListFragment();
|
||||
arguments.putSerializable("type", type);
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public static AccountFragment newInstance(Type type, String accountId) {
|
||||
public static AccountListFragment newInstance(Type type, String accountId) {
|
||||
Bundle arguments = new Bundle();
|
||||
AccountFragment fragment = new AccountFragment();
|
||||
arguments.putString("type", type.name());
|
||||
AccountListFragment fragment = new AccountListFragment();
|
||||
arguments.putSerializable("type", type);
|
||||
arguments.putString("accountId", accountId);
|
||||
fragment.setArguments(arguments);
|
||||
return fragment;
|
||||
|
@ -79,7 +79,7 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
|
|||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
Bundle arguments = getArguments();
|
||||
type = Type.valueOf(arguments.getString("type"));
|
||||
type = (Type) arguments.getSerializable("type");
|
||||
accountId = arguments.getString("accountId");
|
||||
api = null;
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
|
|||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
|
||||
View rootView = inflater.inflate(R.layout.fragment_account, container, false);
|
||||
View rootView = inflater.inflate(R.layout.fragment_account_list, container, false);
|
||||
|
||||
Context context = getContext();
|
||||
recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
|
||||
|
@ -105,6 +105,10 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
|
|||
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);
|
||||
}
|
||||
|
@ -154,12 +158,6 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
|
|||
recyclerView.addOnScrollListener(scrollListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (listCall != null) listCall.cancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (jumpToTopAllowed()) {
|
||||
|
@ -186,6 +184,7 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
|
|||
}
|
||||
};
|
||||
|
||||
Call<List<Account>> listCall;
|
||||
switch (type) {
|
||||
default:
|
||||
case FOLLOWS: {
|
||||
|
@ -204,6 +203,10 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
|
|||
listCall = api.mutes(fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case FOLLOW_REQUESTS: {
|
||||
listCall = api.followRequests(fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
callList.add(listCall);
|
||||
listCall.enqueue(cb);
|
||||
|
@ -236,12 +239,78 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
|
|||
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAccount(String id) {
|
||||
Intent intent = new Intent(getContext(), AccountActivity.class);
|
||||
intent.putExtra("id", id);
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMute(final boolean mute, final String id, final int position) {
|
||||
if (api == null) {
|
||||
/* If somehow an unmute button is clicked after onCreateView but before
|
||||
* onActivityCreated, then this would get called with a null api object, so this eats
|
||||
* that input. */
|
||||
Log.d(TAG, "MastodonAPI isn't initialised so this mute can't occur.");
|
||||
return;
|
||||
}
|
||||
|
||||
Callback<Relationship> callback = new Callback<Relationship>() {
|
||||
@Override
|
||||
public void onResponse(Call<Relationship> call, Response<Relationship> response) {
|
||||
if (response.isSuccessful()) {
|
||||
onMuteSuccess(mute, id, position);
|
||||
} else {
|
||||
onMuteFailure(mute, id);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Relationship> call, 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 = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View 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) {
|
||||
if (api == null) {
|
||||
/* If somehow an unblock button is clicked after onCreateView but before
|
||||
|
@ -255,7 +324,7 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
|
|||
@Override
|
||||
public void onResponse(Call<Relationship> call, Response<Relationship> response) {
|
||||
if (response.isSuccessful()) {
|
||||
onBlockSuccess(block, position);
|
||||
onBlockSuccess(block, id, position);
|
||||
} else {
|
||||
onBlockFailure(block, id);
|
||||
}
|
||||
|
@ -277,9 +346,22 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
|
|||
call.enqueue(cb);
|
||||
}
|
||||
|
||||
private void onBlockSuccess(boolean blocked, int position) {
|
||||
BlocksAdapter blocksAdapter = (BlocksAdapter) adapter;
|
||||
blocksAdapter.setBlocked(blocked, position);
|
||||
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 = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View 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) {
|
||||
|
@ -292,8 +374,56 @@ public class AccountFragment extends BaseFragment implements AccountActionListen
|
|||
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) {
|
||||
if (api == null) {
|
||||
/* If somehow an response button is clicked after onCreateView but before
|
||||
* onActivityCreated, then this would get called with a null api object, so this eats
|
||||
* that input. */
|
||||
Log.d(TAG, "MastodonAPI isn't initialised, so follow requests can't be responded to.");
|
||||
return;
|
||||
}
|
||||
|
||||
Callback<Relationship> callback = new Callback<Relationship>() {
|
||||
@Override
|
||||
public void onResponse(Call<Relationship> call, Response<Relationship> response) {
|
||||
if (response.isSuccessful()) {
|
||||
onRespondToFollowRequestSuccess(position);
|
||||
} else {
|
||||
onRespondToFollowRequestFailure(accept, accountId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Relationship> call, 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 = (accept) ? "accept" : "reject";
|
||||
String message = String.format("Failed to %s account id %s.", verb, accountId);
|
||||
Log.e(TAG, message);
|
||||
}
|
||||
|
||||
private boolean jumpToTopAllowed() {
|
||||
return type != Type.BLOCKS;
|
||||
return type == Type.FOLLOWS || type == Type.FOLLOWERS;
|
||||
}
|
||||
|
||||
private void jumpToTop() {
|
|
@ -24,15 +24,20 @@ import android.view.View;
|
|||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class AccountPagerAdapter extends FragmentPagerAdapter {
|
||||
private Context context;
|
||||
private String accountId;
|
||||
private String[] pageTitles;
|
||||
private List<Fragment> registeredFragments;
|
||||
|
||||
AccountPagerAdapter(FragmentManager manager, Context context, String accountId) {
|
||||
super(manager);
|
||||
this.context = context;
|
||||
this.accountId = accountId;
|
||||
registeredFragments = new ArrayList<>();
|
||||
}
|
||||
|
||||
void setPageTitles(String[] titles) {
|
||||
|
@ -46,10 +51,10 @@ class AccountPagerAdapter extends FragmentPagerAdapter {
|
|||
return TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId);
|
||||
}
|
||||
case 1: {
|
||||
return AccountFragment.newInstance(AccountFragment.Type.FOLLOWS, accountId);
|
||||
return AccountListFragment.newInstance(AccountListFragment.Type.FOLLOWS, accountId);
|
||||
}
|
||||
case 2: {
|
||||
return AccountFragment.newInstance(AccountFragment.Type.FOLLOWERS, accountId);
|
||||
return AccountListFragment.newInstance(AccountListFragment.Type.FOLLOWERS, accountId);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
|
@ -73,4 +78,21 @@ class AccountPagerAdapter extends FragmentPagerAdapter {
|
|||
title.setText(pageTitles[position]);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object instantiateItem(ViewGroup container, int position) {
|
||||
Fragment fragment = (Fragment) super.instantiateItem(container, position);
|
||||
registeredFragments.add(fragment);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||
registeredFragments.remove((Fragment) object);
|
||||
super.destroyItem(container, position, object);
|
||||
}
|
||||
|
||||
List<Fragment> getRegisteredFragments() {
|
||||
return registeredFragments;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
@ -22,6 +24,7 @@ import android.graphics.Color;
|
|||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.os.SystemClock;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
|
@ -29,7 +32,6 @@ import android.text.Spanned;
|
|||
import android.util.TypedValue;
|
||||
import android.view.Menu;
|
||||
|
||||
import com.google.firebase.iid.FirebaseInstanceId;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
|
@ -46,16 +48,13 @@ import retrofit2.Callback;
|
|||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
/* There isn't presently a way to globally change the theme of a whole application at runtime, just
|
||||
* individual activities. So, each activity has to set its theme before any views are created. And
|
||||
* the most expedient way to accomplish this was to put it in a base class and just have every
|
||||
* activity extend from it. */
|
||||
public class BaseActivity extends AppCompatActivity {
|
||||
private static final String TAG = "BaseActivity"; // logging tag
|
||||
|
||||
protected MastodonAPI mastodonAPI;
|
||||
protected TuskyAPI tuskyAPI;
|
||||
protected Dispatcher mastodonApiDispatcher;
|
||||
protected PendingIntent serviceAlarmIntent;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
|
@ -65,6 +64,9 @@ public class BaseActivity extends AppCompatActivity {
|
|||
createMastodonAPI();
|
||||
createTuskyAPI();
|
||||
|
||||
/* There isn't presently a way to globally change the theme of a whole application at
|
||||
* runtime, just individual activities. So, each activity has to set its theme before any
|
||||
* views are created. */
|
||||
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lightTheme", false)) {
|
||||
setTheme(R.style.AppTheme_Light);
|
||||
}
|
||||
|
@ -96,8 +98,12 @@ public class BaseActivity extends AppCompatActivity {
|
|||
overridePendingTransition(R.anim.slide_from_left, R.anim.slide_to_right);
|
||||
}
|
||||
|
||||
protected SharedPreferences getPrivatePreferences() {
|
||||
return getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
protected String getAccessToken() {
|
||||
SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
SharedPreferences preferences = getPrivatePreferences();
|
||||
return preferences.getString("accessToken", null);
|
||||
}
|
||||
|
||||
|
@ -107,7 +113,7 @@ public class BaseActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
protected String getBaseUrl() {
|
||||
SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
SharedPreferences preferences = getPrivatePreferences();
|
||||
return "https://" + preferences.getString("domain", null);
|
||||
}
|
||||
|
||||
|
@ -116,6 +122,7 @@ public class BaseActivity extends AppCompatActivity {
|
|||
|
||||
Gson gson = new GsonBuilder()
|
||||
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
|
||||
.registerTypeAdapter(StringWithEmoji.class, new StringWithEmojiTypeAdapter())
|
||||
.create();
|
||||
|
||||
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder()
|
||||
|
@ -148,17 +155,18 @@ public class BaseActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
protected void createTuskyAPI() {
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl(getString(R.string.tusky_api_url))
|
||||
.client(OkHttpUtils.getCompatibleClient())
|
||||
.build();
|
||||
if (BuildConfig.USES_PUSH_NOTIFICATIONS) {
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl(getString(R.string.tusky_api_url))
|
||||
.client(OkHttpUtils.getCompatibleClient())
|
||||
.build();
|
||||
|
||||
tuskyAPI = retrofit.create(TuskyAPI.class);
|
||||
tuskyAPI = retrofit.create(TuskyAPI.class);
|
||||
}
|
||||
}
|
||||
|
||||
protected void redirectIfNotLoggedIn() {
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
SharedPreferences preferences = getPrivatePreferences();
|
||||
String domain = preferences.getString("domain", null);
|
||||
String accessToken = preferences.getString("accessToken", null);
|
||||
if (domain == null || accessToken == null) {
|
||||
|
@ -188,30 +196,49 @@ public class BaseActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
protected void enablePushNotifications() {
|
||||
tuskyAPI.register(getBaseUrl(), getAccessToken(), FirebaseInstanceId.getInstance().getToken()).enqueue(new Callback<ResponseBody>() {
|
||||
@Override
|
||||
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
|
||||
Log.d(TAG, "Enable push notifications response: " + response.message());
|
||||
}
|
||||
if (BuildConfig.USES_PUSH_NOTIFICATIONS) {
|
||||
String token = com.google.firebase.iid.FirebaseInstanceId.getInstance().getToken();
|
||||
tuskyAPI.register(getBaseUrl(), getAccessToken(), token).enqueue(new Callback<ResponseBody>() {
|
||||
@Override
|
||||
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
|
||||
Log.d(TAG, "Enable push notifications response: " + response.message());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ResponseBody> call, Throwable t) {
|
||||
Log.d(TAG, "Enable push notifications failed: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onFailure(Call<ResponseBody> call, Throwable t) {
|
||||
Log.d(TAG, "Enable push notifications failed: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Start up the MessagingService on a repeating interval for "pull" notifications.
|
||||
long checkInterval = 60 * 1000 * 5;
|
||||
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
|
||||
Intent intent = new Intent(this, MessagingService.class);
|
||||
final int SERVICE_REQUEST_CODE = 8574603; // This number is arbitrary.
|
||||
serviceAlarmIntent = PendingIntent.getService(this, SERVICE_REQUEST_CODE, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
||||
SystemClock.elapsedRealtime(), checkInterval, serviceAlarmIntent);
|
||||
}
|
||||
}
|
||||
|
||||
protected void disablePushNotifications() {
|
||||
tuskyAPI.unregister(getBaseUrl(), getAccessToken()).enqueue(new Callback<ResponseBody>() {
|
||||
@Override
|
||||
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
|
||||
Log.d(TAG, "Disable push notifications response: " + response.message());
|
||||
}
|
||||
if (BuildConfig.USES_PUSH_NOTIFICATIONS) {
|
||||
tuskyAPI.unregister(getBaseUrl(), getAccessToken()).enqueue(new Callback<ResponseBody>() {
|
||||
@Override
|
||||
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
|
||||
Log.d(TAG, "Disable push notifications response: " + response.message());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ResponseBody> call, Throwable t) {
|
||||
Log.d(TAG, "Disable push notifications failed: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
@Override
|
||||
public void onFailure(Call<ResponseBody> call, Throwable t) {
|
||||
Log.d(TAG, "Disable push notifications failed: " + t.getMessage());
|
||||
}
|
||||
});
|
||||
} else if (serviceAlarmIntent != null) {
|
||||
// Cancel the repeating call for "pull" notifications.
|
||||
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
|
||||
alarmManager.cancel(serviceAlarmIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
|
@ -40,4 +42,9 @@ public class BaseFragment extends Fragment {
|
|||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
protected SharedPreferences getPrivatePreferences() {
|
||||
return getContext().getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,9 +26,6 @@ import com.keylesspalace.tusky.entity.Account;
|
|||
import com.pkmmte.view.CircularImageView;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
|
||||
|
@ -36,11 +33,8 @@ class BlocksAdapter extends AccountAdapter {
|
|||
private static final int VIEW_TYPE_BLOCKED_USER = 0;
|
||||
private static final int VIEW_TYPE_FOOTER = 1;
|
||||
|
||||
private Set<Integer> unblockedAccountPositions;
|
||||
|
||||
BlocksAdapter(AccountActionListener accountActionListener) {
|
||||
super(accountActionListener);
|
||||
unblockedAccountPositions = new HashSet<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -65,8 +59,7 @@ class BlocksAdapter extends AccountAdapter {
|
|||
if (position < accountList.size()) {
|
||||
BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position));
|
||||
boolean blocked = !unblockedAccountPositions.contains(position);
|
||||
holder.setupActionListener(accountActionListener, blocked, position);
|
||||
holder.setupActionListener(accountActionListener, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -79,15 +72,6 @@ class BlocksAdapter extends AccountAdapter {
|
|||
}
|
||||
}
|
||||
|
||||
void setBlocked(boolean blocked, int position) {
|
||||
if (blocked) {
|
||||
unblockedAccountPositions.remove(position);
|
||||
} else {
|
||||
unblockedAccountPositions.add(position);
|
||||
}
|
||||
notifyItemChanged(position);
|
||||
}
|
||||
|
||||
static class BlockedUserViewHolder extends RecyclerView.ViewHolder {
|
||||
@BindView(R.id.blocked_user_avatar) CircularImageView avatar;
|
||||
@BindView(R.id.blocked_user_username) TextView username;
|
||||
|
@ -114,12 +98,14 @@ class BlocksAdapter extends AccountAdapter {
|
|||
.into(avatar);
|
||||
}
|
||||
|
||||
void setupActionListener(final AccountActionListener listener, final boolean blocked,
|
||||
final int position) {
|
||||
void setupActionListener(final AccountActionListener listener, final boolean blocked) {
|
||||
unblock.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onBlock(!blocked, id, position);
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onBlock(!blocked, id, position);
|
||||
}
|
||||
}
|
||||
});
|
||||
avatar.setOnClickListener(new View.OnClickListener() {
|
||||
|
|
|
@ -18,7 +18,6 @@ package com.keylesspalace.tusky;
|
|||
import android.Manifest;
|
||||
import android.app.ProgressDialog;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
|
@ -36,8 +35,10 @@ import android.net.Uri;
|
|||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.MediaStore;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
@ -48,6 +49,7 @@ import android.support.v13.view.inputmethod.InputConnectionCompat;
|
|||
import android.support.v13.view.inputmethod.InputContentInfoCompat;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.content.FileProvider;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.content.res.AppCompatResources;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
|
@ -77,9 +79,11 @@ import com.keylesspalace.tusky.entity.Media;
|
|||
import com.keylesspalace.tusky.entity.Status;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
|
@ -99,8 +103,11 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
private static final int STATUS_CHARACTER_LIMIT = 500;
|
||||
private static final int STATUS_MEDIA_SIZE_LIMIT = 4000000; // 4MB
|
||||
private static final int MEDIA_PICK_RESULT = 1;
|
||||
private static final int MEDIA_TAKE_PHOTO_RESULT = 2;
|
||||
private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1;
|
||||
private static final int MEDIA_SIZE_UNKNOWN = -1;
|
||||
private static final int COMPOSE_SUCCESS = -1;
|
||||
private static final int THUMBNAIL_SIZE = 128; // pixels
|
||||
|
||||
private String inReplyToId;
|
||||
private EditText textEditor;
|
||||
|
@ -120,8 +127,11 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
private TextView charactersLeft;
|
||||
private Button floatingBtn;
|
||||
private ImageButton pickBtn;
|
||||
private ImageButton takeBtn;
|
||||
private Button nsfwBtn;
|
||||
private ProgressBar postProgress;
|
||||
private ImageButton visibilityBtn;
|
||||
private Uri photoUploadUri;
|
||||
|
||||
private static class QueuedMedia {
|
||||
enum Type {
|
||||
|
@ -335,23 +345,17 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
actionBar.setHomeAsUpIndicator(closeIcon);
|
||||
}
|
||||
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
SharedPreferences preferences = getPrivatePreferences();
|
||||
|
||||
floatingBtn = (Button) findViewById(R.id.floating_btn);
|
||||
pickBtn = (ImageButton) findViewById(R.id.compose_photo_pick);
|
||||
takeBtn = (ImageButton) findViewById(R.id.compose_photo_take);
|
||||
nsfwBtn = (Button) findViewById(R.id.action_toggle_nsfw);
|
||||
final ImageButton visibilityBtn = (ImageButton) findViewById(R.id.action_toggle_visibility);
|
||||
visibilityBtn = (ImageButton) findViewById(R.id.action_toggle_visibility);
|
||||
|
||||
floatingBtn.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
pickBtn.setClickable(false);
|
||||
nsfwBtn.setClickable(false);
|
||||
visibilityBtn.setClickable(false);
|
||||
floatingBtn.setEnabled(false);
|
||||
|
||||
postProgress.setVisibility(View.VISIBLE);
|
||||
sendStatus();
|
||||
}
|
||||
});
|
||||
|
@ -361,6 +365,12 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
onMediaPick();
|
||||
}
|
||||
});
|
||||
takeBtn.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
initiateCameraApp();
|
||||
}
|
||||
});
|
||||
nsfwBtn.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
|
@ -567,26 +577,69 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
}
|
||||
}
|
||||
|
||||
private void disableButtons() {
|
||||
pickBtn.setClickable(false);
|
||||
takeBtn.setClickable(false);
|
||||
nsfwBtn.setClickable(false);
|
||||
visibilityBtn.setClickable(false);
|
||||
floatingBtn.setEnabled(false);
|
||||
}
|
||||
|
||||
private void enableButtons() {
|
||||
pickBtn.setClickable(true);
|
||||
takeBtn.setClickable(true);
|
||||
nsfwBtn.setClickable(true);
|
||||
visibilityBtn.setClickable(true);
|
||||
floatingBtn.setEnabled(true);
|
||||
}
|
||||
|
||||
private void addLockToSendButton() {
|
||||
floatingBtn.setText(R.string.action_send);
|
||||
Drawable lock = AppCompatResources.getDrawable(this, R.drawable.send_private);
|
||||
if (lock != null) {
|
||||
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
|
||||
floatingBtn.setCompoundDrawables(null, null, lock, null);
|
||||
}
|
||||
}
|
||||
|
||||
private void setStatusVisibility(String visibility) {
|
||||
statusVisibility = visibility;
|
||||
switch (visibility) {
|
||||
case "public": {
|
||||
floatingBtn.setText(R.string.action_send_public);
|
||||
floatingBtn.setCompoundDrawables(null, null, null, null);
|
||||
break;
|
||||
}
|
||||
case "private": {
|
||||
floatingBtn.setText(R.string.action_send);
|
||||
Drawable lock = AppCompatResources.getDrawable(this, R.drawable.send_private);
|
||||
if (lock != null) {
|
||||
lock.setBounds(0, 0, lock.getIntrinsicWidth(), lock.getIntrinsicHeight());
|
||||
floatingBtn.setCompoundDrawables(null, null, lock, null);
|
||||
Drawable globe = AppCompatResources.getDrawable(this, R.drawable.ic_public_24dp);
|
||||
if (globe != null) {
|
||||
visibilityBtn.setImageDrawable(globe);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "private": {
|
||||
addLockToSendButton();
|
||||
Drawable lock = AppCompatResources.getDrawable(this,
|
||||
R.drawable.ic_lock_outline_24dp);
|
||||
if (lock != null) {
|
||||
visibilityBtn.setImageDrawable(lock);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "direct": {
|
||||
addLockToSendButton();
|
||||
Drawable envelope = AppCompatResources.getDrawable(this, R.drawable.ic_email_24dp);
|
||||
if (envelope != null) {
|
||||
visibilityBtn.setImageDrawable(envelope);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "unlisted":
|
||||
default: {
|
||||
floatingBtn.setText(R.string.action_send);
|
||||
floatingBtn.setCompoundDrawables(null, null, null, null);
|
||||
Drawable openLock = AppCompatResources.getDrawable(this,
|
||||
R.drawable.ic_lock_open_24dp);
|
||||
if (openLock != null) {
|
||||
visibilityBtn.setImageDrawable(openLock);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -615,6 +668,18 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
updateVisibleCharactersLeft();
|
||||
}
|
||||
|
||||
void setStateToReadying() {
|
||||
statusAlreadyInFlight = true;
|
||||
disableButtons();
|
||||
postProgress.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
void setStateToNotReadying() {
|
||||
postProgress.setVisibility(View.INVISIBLE);
|
||||
statusAlreadyInFlight = false;
|
||||
enableButtons();
|
||||
}
|
||||
|
||||
private void sendStatus() {
|
||||
if (statusAlreadyInFlight) {
|
||||
return;
|
||||
|
@ -624,9 +689,12 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
if (statusHideText) {
|
||||
spoilerText = contentWarningEditor.getText().toString();
|
||||
}
|
||||
if (contentText.length() + spoilerText.length() <= STATUS_CHARACTER_LIMIT) {
|
||||
statusAlreadyInFlight = true;
|
||||
int characterCount = contentText.length() + spoilerText.length();
|
||||
if (characterCount > 0 && characterCount <= STATUS_CHARACTER_LIMIT) {
|
||||
setStateToReadying();
|
||||
readyStatus(contentText, statusVisibility, statusMarkSensitive, spoilerText);
|
||||
} else if (characterCount <= 0) {
|
||||
textEditor.setError(getString(R.string.error_empty));
|
||||
} else {
|
||||
textEditor.setError(getString(R.string.error_compose_character_limit));
|
||||
}
|
||||
|
@ -685,11 +753,9 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
* the status they reply to and that behaviour needs to be kept separate. */
|
||||
return;
|
||||
}
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
editor.putString("rememberedVisibility", statusVisibility);
|
||||
editor.apply();
|
||||
getPrivatePreferences().edit()
|
||||
.putString("rememberedVisibility", statusVisibility)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private EditText createEditText(String[] contentMimeTypes) {
|
||||
|
@ -719,7 +785,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
editText.setLayoutParams(layoutParams);
|
||||
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
|
||||
editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
|
||||
editText.setEms(10);
|
||||
editText.setBackgroundColor(0);
|
||||
editText.setGravity(Gravity.START | Gravity.TOP);
|
||||
|
@ -816,13 +882,13 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
private void onSendSuccess() {
|
||||
Snackbar bar = Snackbar.make(findViewById(R.id.activity_compose), getString(R.string.confirmation_send), Snackbar.LENGTH_SHORT);
|
||||
bar.show();
|
||||
setResult(COMPOSE_SUCCESS);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void onSendFailure() {
|
||||
postProgress.setVisibility(View.INVISIBLE);
|
||||
textEditor.setError(getString(R.string.error_generic));
|
||||
statusAlreadyInFlight = false;
|
||||
setStateToNotReadying();
|
||||
}
|
||||
|
||||
private void readyStatus(final String content, final String visibility, final boolean sensitive,
|
||||
|
@ -857,7 +923,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
@Override
|
||||
protected void onCancelled() {
|
||||
removeAllMediaFromQueue();
|
||||
statusAlreadyInFlight = false;
|
||||
setStateToNotReadying();
|
||||
super.onCancelled();
|
||||
}
|
||||
};
|
||||
|
@ -882,7 +948,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
readyStatus(content, visibility, sensitive, spoilerText);
|
||||
}
|
||||
});
|
||||
statusAlreadyInFlight = false;
|
||||
setStateToNotReadying();
|
||||
}
|
||||
|
||||
private void onMediaPick() {
|
||||
|
@ -919,6 +985,41 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
}
|
||||
}
|
||||
|
||||
private File createNewImageFile() throws IOException {
|
||||
// Create an image file name
|
||||
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
|
||||
String imageFileName = "Tusky_" + timeStamp + "_";
|
||||
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
|
||||
return File.createTempFile(
|
||||
imageFileName, /* prefix */
|
||||
".jpg", /* suffix */
|
||||
storageDir /* directory */
|
||||
);
|
||||
}
|
||||
|
||||
private void initiateCameraApp() {
|
||||
// We don't need to ask for permission in this case, because the used calls require
|
||||
// android.permission.WRITE_EXTERNAL_STORAGE only on SDKs *older* than Kitkat, which was
|
||||
// way before permission dialogues have been introduced.
|
||||
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
||||
if (intent.resolveActivity(getPackageManager()) != null) {
|
||||
File photoFile = null;
|
||||
try {
|
||||
photoFile = createNewImageFile();
|
||||
} catch (IOException ex) {
|
||||
displayTransientError(R.string.error_media_upload_opening);
|
||||
}
|
||||
// Continue only if the File was successfully created
|
||||
if (photoFile != null) {
|
||||
photoUploadUri = FileProvider.getUriForFile(this,
|
||||
"com.keylesspalace.tusky.fileprovider",
|
||||
photoFile);
|
||||
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUploadUri);
|
||||
startActivityForResult(intent, MEDIA_TAKE_PHOTO_RESULT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initiateMediaPicking() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
|
@ -932,16 +1033,22 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
startActivityForResult(intent, MEDIA_PICK_RESULT);
|
||||
}
|
||||
|
||||
private void enableMediaPicking() {
|
||||
private void enableMediaButtons() {
|
||||
pickBtn.setEnabled(true);
|
||||
ThemeUtils.setDrawableTint(this, pickBtn.getDrawable(),
|
||||
R.attr.compose_media_button_tint);
|
||||
takeBtn.setEnabled(true);
|
||||
ThemeUtils.setDrawableTint(this, takeBtn.getDrawable(),
|
||||
R.attr.compose_media_button_tint);
|
||||
}
|
||||
|
||||
private void disableMediaPicking() {
|
||||
private void disableMediaButtons() {
|
||||
pickBtn.setEnabled(false);
|
||||
ThemeUtils.setDrawableTint(this, pickBtn.getDrawable(),
|
||||
R.attr.compose_media_button_disabled_tint);
|
||||
takeBtn.setEnabled(false);
|
||||
ThemeUtils.setDrawableTint(this, takeBtn.getDrawable(),
|
||||
R.attr.compose_media_button_disabled_tint);
|
||||
}
|
||||
|
||||
private void addMediaToQueue(QueuedMedia.Type type, Bitmap preview, Uri uri, long mediaSize) {
|
||||
|
@ -976,11 +1083,11 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
textEditor.getPaddingRight(), totalHeight);
|
||||
// If there's one video in the queue it is full, so disable the button to queue more.
|
||||
if (item.type == QueuedMedia.Type.VIDEO) {
|
||||
disableMediaPicking();
|
||||
disableMediaButtons();
|
||||
}
|
||||
} else if (queuedCount >= Status.MAX_MEDIA_ATTACHMENTS) {
|
||||
// Limit the total media attachments, also.
|
||||
disableMediaPicking();
|
||||
disableMediaButtons();
|
||||
}
|
||||
if (queuedCount >= 1) {
|
||||
showMarkSensitive(true);
|
||||
|
@ -1003,7 +1110,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
textEditor.setPadding(textEditor.getPaddingLeft(), textEditor.getPaddingTop(),
|
||||
textEditor.getPaddingRight(), 0);
|
||||
}
|
||||
enableMediaPicking();
|
||||
enableMediaButtons();
|
||||
cancelReadyingMedia(item);
|
||||
}
|
||||
|
||||
|
@ -1164,6 +1271,9 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
Uri uri = data.getData();
|
||||
long mediaSize = getMediaSize(getContentResolver(), uri);
|
||||
pickMedia(uri, mediaSize);
|
||||
} else if (requestCode == MEDIA_TAKE_PHOTO_RESULT && resultCode == RESULT_OK) {
|
||||
long mediaSize = getMediaSize(getContentResolver(), photoUploadUri);
|
||||
pickMedia(photoUploadUri, mediaSize);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1190,7 +1300,7 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
|
||||
retriever.setDataSource(this, uri);
|
||||
Bitmap source = retriever.getFrameAtTime();
|
||||
Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, 128, 128);
|
||||
Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, THUMBNAIL_SIZE, THUMBNAIL_SIZE);
|
||||
source.recycle();
|
||||
addMediaToQueue(QueuedMedia.Type.VIDEO, bitmap, uri, mediaSize);
|
||||
break;
|
||||
|
@ -1203,8 +1313,9 @@ public class ComposeActivity extends BaseActivity implements ComposeOptionsFrag
|
|||
displayTransientError(R.string.error_media_upload_opening);
|
||||
return;
|
||||
}
|
||||
|
||||
Bitmap source = BitmapFactory.decodeStream(stream);
|
||||
Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, 128, 128);
|
||||
Bitmap bitmap = ThumbnailUtils.extractThumbnail(source, THUMBNAIL_SIZE, THUMBNAIL_SIZE);
|
||||
source.recycle();
|
||||
try {
|
||||
if (stream != null) {
|
||||
|
|
|
@ -73,10 +73,14 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment {
|
|||
radioCheckedId = R.id.radio_unlisted;
|
||||
}
|
||||
if (statusVisibility != null) {
|
||||
if (statusVisibility.equals("unlisted")) {
|
||||
radioCheckedId = R.id.radio_unlisted;
|
||||
if (statusVisibility.equals("public")) {
|
||||
radioCheckedId = R.id.radio_public;
|
||||
} else if (statusVisibility.equals("private")) {
|
||||
radioCheckedId = R.id.radio_private;
|
||||
} else if (statusVisibility.equals("unlisted")) {
|
||||
radioCheckedId = R.id.radio_unlisted;
|
||||
} else if (statusVisibility.equals("direct")) {
|
||||
radioCheckedId = R.id.radio_direct;
|
||||
}
|
||||
}
|
||||
radio.check(radioCheckedId);
|
||||
|
@ -113,6 +117,10 @@ public class ComposeOptionsFragment extends BottomSheetDialogFragment {
|
|||
visibility = "private";
|
||||
break;
|
||||
}
|
||||
case R.id.radio_direct: {
|
||||
visibility = "direct";
|
||||
break;
|
||||
}
|
||||
}
|
||||
listener.onVisibilityChanged(visibility);
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ class CustomTabURLSpan extends URLSpan {
|
|||
customTabsIntent.launchUrl(context, uri);
|
||||
}
|
||||
} catch (ActivityNotFoundException e) {
|
||||
android.util.Log.w("URLSpan", "Activity was not found for intent, " + customTabsIntent.toString());
|
||||
Log.w("URLSpan", "Activity was not found for intent, " + customTabsIntent.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.graphics.BitmapFactory;
|
|||
import android.graphics.Matrix;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.media.ExifInterface;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
@ -42,6 +43,7 @@ class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
|||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Bitmap reorientBitmap(Bitmap bitmap, int orientation) {
|
||||
Matrix matrix = new Matrix();
|
||||
switch (orientation) {
|
||||
|
|
|
@ -0,0 +1,515 @@
|
|||
/* 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.Manifest;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.ActionBar;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Base64;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ProgressBar;
|
||||
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.Profile;
|
||||
import com.theartofdev.edmodo.cropper.CropImage;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class EditProfileActivity extends BaseActivity {
|
||||
private static final String TAG = "EditProfileActivity";
|
||||
private static final int AVATAR_PICK_RESULT = 1;
|
||||
private static final int HEADER_PICK_RESULT = 2;
|
||||
private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1;
|
||||
private static final int AVATAR_WIDTH = 120;
|
||||
private static final int AVATAR_HEIGHT = 120;
|
||||
private static final int HEADER_WIDTH = 700;
|
||||
private static final int HEADER_HEIGHT = 335;
|
||||
|
||||
private enum PickType {
|
||||
NOTHING,
|
||||
AVATAR,
|
||||
HEADER
|
||||
}
|
||||
|
||||
@BindView(R.id.edit_profile_display_name) EditText displayNameEditText;
|
||||
@BindView(R.id.edit_profile_note) EditText noteEditText;
|
||||
@BindView(R.id.edit_profile_avatar) Button avatarButton;
|
||||
@BindView(R.id.edit_profile_avatar_preview) ImageView avatarPreview;
|
||||
@BindView(R.id.edit_profile_avatar_progress) ProgressBar avatarProgress;
|
||||
@BindView(R.id.edit_profile_header) Button headerButton;
|
||||
@BindView(R.id.edit_profile_header_preview) ImageView headerPreview;
|
||||
@BindView(R.id.edit_profile_header_progress) ProgressBar headerProgress;
|
||||
@BindView(R.id.edit_profile_save_progress) ProgressBar saveProgress;
|
||||
|
||||
private String priorDisplayName;
|
||||
private String priorNote;
|
||||
private boolean isAlreadySaving;
|
||||
private PickType currentlyPicking;
|
||||
private String avatarBase64;
|
||||
private String headerBase64;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_edit_profile);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setTitle(null);
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
priorDisplayName = savedInstanceState.getString("priorDisplayName");
|
||||
priorNote = savedInstanceState.getString("priorNote");
|
||||
isAlreadySaving = savedInstanceState.getBoolean("isAlreadySaving");
|
||||
currentlyPicking = (PickType) savedInstanceState.getSerializable("currentlyPicking");
|
||||
avatarBase64 = savedInstanceState.getString("avatarBase64");
|
||||
headerBase64 = savedInstanceState.getString("headerBase64");
|
||||
} else {
|
||||
priorDisplayName = null;
|
||||
priorNote = null;
|
||||
isAlreadySaving = false;
|
||||
currentlyPicking = PickType.NOTHING;
|
||||
avatarBase64 = null;
|
||||
headerBase64 = null;
|
||||
}
|
||||
|
||||
avatarButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
onMediaPick(PickType.AVATAR);
|
||||
}
|
||||
});
|
||||
headerButton.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
onMediaPick(PickType.HEADER);
|
||||
}
|
||||
});
|
||||
|
||||
avatarPreview.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
avatarPreview.setImageBitmap(null);
|
||||
avatarPreview.setVisibility(View.GONE);
|
||||
avatarBase64 = null;
|
||||
}
|
||||
});
|
||||
headerPreview.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
headerPreview.setImageBitmap(null);
|
||||
headerPreview.setVisibility(View.GONE);
|
||||
headerBase64 = null;
|
||||
}
|
||||
});
|
||||
|
||||
mastodonAPI.accountVerifyCredentials().enqueue(new Callback<Account>() {
|
||||
@Override
|
||||
public void onResponse(Call<Account> call, Response<Account> response) {
|
||||
if (!response.isSuccessful()) {
|
||||
onAccountVerifyCredentialsFailed();
|
||||
return;
|
||||
}
|
||||
Account me = response.body();
|
||||
priorDisplayName = me.getDisplayName();
|
||||
priorNote = me.note.toString();
|
||||
displayNameEditText.setText(priorDisplayName);
|
||||
noteEditText.setText(priorNote);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Account> call, Throwable t) {
|
||||
onAccountVerifyCredentialsFailed();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
outState.putString("priorDisplayName", priorDisplayName);
|
||||
outState.putString("priorNote", priorNote);
|
||||
outState.putBoolean("isAlreadySaving", isAlreadySaving);
|
||||
outState.putSerializable("currentlyPicking", currentlyPicking);
|
||||
outState.putString("avatarBase64", avatarBase64);
|
||||
outState.putString("headerBase64", headerBase64);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
private void onAccountVerifyCredentialsFailed() {
|
||||
Log.e(TAG, "The account failed to load.");
|
||||
}
|
||||
|
||||
private void onMediaPick(PickType pickType) {
|
||||
if (currentlyPicking != PickType.NOTHING) {
|
||||
// Ignore inputs if another pick operation is still occurring.
|
||||
return;
|
||||
}
|
||||
currentlyPicking = pickType;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
|
||||
ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[] { Manifest.permission.READ_EXTERNAL_STORAGE },
|
||||
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE);
|
||||
} else {
|
||||
initiateMediaPicking();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
|
||||
@NonNull int[] grantResults) {
|
||||
switch (requestCode) {
|
||||
case PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE: {
|
||||
if (grantResults.length > 0
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
initiateMediaPicking();
|
||||
} else {
|
||||
endMediaPicking();
|
||||
Snackbar.make(avatarButton, R.string.error_media_upload_permission,
|
||||
Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initiateMediaPicking() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("image/*");
|
||||
switch (currentlyPicking) {
|
||||
case AVATAR: { startActivityForResult(intent, AVATAR_PICK_RESULT); break; }
|
||||
case HEADER: { startActivityForResult(intent, HEADER_PICK_RESULT); break; }
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.edit_profile_toolbar, menu);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home: {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
case R.id.action_save: {
|
||||
save();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void save() {
|
||||
if (isAlreadySaving || currentlyPicking != PickType.NOTHING) {
|
||||
return;
|
||||
}
|
||||
String newDisplayName = displayNameEditText.getText().toString();
|
||||
if (newDisplayName.isEmpty()) {
|
||||
displayNameEditText.setError(getString(R.string.error_empty));
|
||||
return;
|
||||
}
|
||||
if (priorDisplayName != null && priorDisplayName.equals(newDisplayName)) {
|
||||
// If it's not any different, don't patch it.
|
||||
newDisplayName = null;
|
||||
}
|
||||
|
||||
String newNote = noteEditText.getText().toString();
|
||||
if (newNote.isEmpty()) {
|
||||
noteEditText.setError(getString(R.string.error_empty));
|
||||
return;
|
||||
}
|
||||
if (priorNote != null && priorNote.equals(newNote)) {
|
||||
// If it's not any different, don't patch it.
|
||||
newNote = null;
|
||||
}
|
||||
if (newDisplayName == null && newNote == null && avatarBase64 == null
|
||||
&& headerBase64 == null) {
|
||||
// If nothing is changed, then there's nothing to save.
|
||||
return;
|
||||
}
|
||||
|
||||
saveProgress.setVisibility(View.VISIBLE);
|
||||
|
||||
isAlreadySaving = true;
|
||||
|
||||
Profile profile = new Profile();
|
||||
profile.displayName = newDisplayName;
|
||||
profile.note = newNote;
|
||||
profile.avatar = avatarBase64;
|
||||
profile.header = headerBase64;
|
||||
mastodonAPI.accountUpdateCredentials(profile).enqueue(new Callback<Account>() {
|
||||
@Override
|
||||
public void onResponse(Call<Account> call, Response<Account> response) {
|
||||
if (!response.isSuccessful()) {
|
||||
onSaveFailure();
|
||||
return;
|
||||
}
|
||||
getPrivatePreferences().edit()
|
||||
.putBoolean("refreshProfileHeader", true)
|
||||
.apply();
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Account> call, Throwable t) {
|
||||
onSaveFailure();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onSaveFailure() {
|
||||
isAlreadySaving = false;
|
||||
Snackbar.make(avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG)
|
||||
.show();
|
||||
saveProgress.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
private void beginMediaPicking() {
|
||||
switch (currentlyPicking) {
|
||||
case AVATAR: {
|
||||
avatarProgress.setVisibility(View.VISIBLE);
|
||||
avatarPreview.setVisibility(View.INVISIBLE);
|
||||
break;
|
||||
}
|
||||
case HEADER: {
|
||||
headerProgress.setVisibility(View.VISIBLE);
|
||||
headerPreview.setVisibility(View.INVISIBLE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void endMediaPicking() {
|
||||
switch (currentlyPicking) {
|
||||
case AVATAR: {
|
||||
avatarProgress.setVisibility(View.GONE);
|
||||
avatarPreview.setVisibility(View.GONE);
|
||||
break;
|
||||
}
|
||||
case HEADER: {
|
||||
headerProgress.setVisibility(View.GONE);
|
||||
headerPreview.setVisibility(View.GONE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
currentlyPicking = PickType.NOTHING;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
switch (requestCode) {
|
||||
case AVATAR_PICK_RESULT: {
|
||||
if (resultCode == RESULT_OK && data != null) {
|
||||
CropImage.activity(data.getData())
|
||||
.setInitialCropWindowPaddingRatio(0)
|
||||
.setAspectRatio(AVATAR_WIDTH, AVATAR_HEIGHT)
|
||||
.start(this);
|
||||
} else {
|
||||
endMediaPicking();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HEADER_PICK_RESULT: {
|
||||
if (resultCode == RESULT_OK && data != null) {
|
||||
CropImage.activity(data.getData())
|
||||
.setInitialCropWindowPaddingRatio(0)
|
||||
.setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
|
||||
.start(this);
|
||||
} else {
|
||||
endMediaPicking();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE: {
|
||||
CropImage.ActivityResult result = CropImage.getActivityResult(data);
|
||||
if (resultCode == RESULT_OK) {
|
||||
beginResize(result.getUri());
|
||||
} else if (resultCode == CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE) {
|
||||
onResizeFailure();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void beginResize(Uri uri) {
|
||||
beginMediaPicking();
|
||||
int width, height;
|
||||
switch (currentlyPicking) {
|
||||
default: {
|
||||
throw new AssertionError("PickType not set.");
|
||||
}
|
||||
case AVATAR: {
|
||||
width = AVATAR_WIDTH;
|
||||
height = AVATAR_HEIGHT;
|
||||
break;
|
||||
}
|
||||
case HEADER: {
|
||||
width = HEADER_WIDTH;
|
||||
height = HEADER_HEIGHT;
|
||||
break;
|
||||
}
|
||||
}
|
||||
new ResizeImageTask(getContentResolver(), width, height, new ResizeImageTask.Listener() {
|
||||
@Override
|
||||
public void onSuccess(List<Bitmap> contentList) {
|
||||
Bitmap bitmap = contentList.get(0);
|
||||
PickType pickType = currentlyPicking;
|
||||
endMediaPicking();
|
||||
switch (pickType) {
|
||||
case AVATAR: {
|
||||
avatarPreview.setImageBitmap(bitmap);
|
||||
avatarPreview.setVisibility(View.VISIBLE);
|
||||
avatarBase64 = bitmapToBase64(bitmap);
|
||||
break;
|
||||
}
|
||||
case HEADER: {
|
||||
headerPreview.setImageBitmap(bitmap);
|
||||
headerPreview.setVisibility(View.VISIBLE);
|
||||
headerBase64 = bitmapToBase64(bitmap);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
onResizeFailure();
|
||||
}
|
||||
}).execute(uri);
|
||||
}
|
||||
|
||||
private void onResizeFailure() {
|
||||
Snackbar.make(avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG)
|
||||
.show();
|
||||
endMediaPicking();
|
||||
}
|
||||
|
||||
private static String bitmapToBase64(Bitmap bitmap) {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
||||
byte[] byteArray = stream.toByteArray();
|
||||
IOUtils.closeQuietly(stream);
|
||||
return "data:image/png;base64," + Base64.encodeToString(byteArray, Base64.DEFAULT);
|
||||
}
|
||||
|
||||
private static class ResizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||
private ContentResolver contentResolver;
|
||||
private int resizeWidth;
|
||||
private int resizeHeight;
|
||||
private Listener listener;
|
||||
private List<Bitmap> resultList;
|
||||
|
||||
ResizeImageTask(ContentResolver contentResolver, int width, int height, Listener listener) {
|
||||
this.contentResolver = contentResolver;
|
||||
this.resizeWidth = width;
|
||||
this.resizeHeight = height;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Boolean doInBackground(Uri... uris) {
|
||||
resultList = new ArrayList<>();
|
||||
for (Uri uri : uris) {
|
||||
InputStream inputStream;
|
||||
try {
|
||||
inputStream = contentResolver.openInputStream(uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
Bitmap sourceBitmap;
|
||||
try {
|
||||
sourceBitmap = BitmapFactory.decodeStream(inputStream, null, null);
|
||||
} catch (OutOfMemoryError error) {
|
||||
return false;
|
||||
} finally {
|
||||
IOUtils.closeQuietly(inputStream);
|
||||
}
|
||||
if (sourceBitmap == null) {
|
||||
return false;
|
||||
}
|
||||
Bitmap bitmap = Bitmap.createScaledBitmap(sourceBitmap, resizeWidth, resizeHeight,
|
||||
false);
|
||||
sourceBitmap.recycle();
|
||||
if (bitmap == null) {
|
||||
return false;
|
||||
}
|
||||
resultList.add(bitmap);
|
||||
if (isCancelled()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Boolean successful) {
|
||||
if (successful) {
|
||||
listener.onSuccess(resultList);
|
||||
} else {
|
||||
listener.onFailure();
|
||||
}
|
||||
super.onPostExecute(successful);
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
void onSuccess(List<Bitmap> contentList);
|
||||
void onFailure();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,7 +23,9 @@ import android.support.v7.app.ActionBar;
|
|||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuItem;
|
||||
|
||||
public class FavouritesActivity extends BaseActivity {
|
||||
public class FavouritesActivity extends BaseActivity implements SFragment.OnUserRemovedListener {
|
||||
private StatusRemoveListener statusRemoveListener;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
@ -42,6 +44,8 @@ public class FavouritesActivity extends BaseActivity {
|
|||
Fragment fragment = TimelineFragment.newInstance(TimelineFragment.Kind.FAVOURITES);
|
||||
fragmentTransaction.add(R.id.fragment_container, fragment);
|
||||
fragmentTransaction.commit();
|
||||
|
||||
statusRemoveListener = (StatusRemoveListener) fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -54,4 +58,9 @@ public class FavouritesActivity extends BaseActivity {
|
|||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserRemoved(String accountId) {
|
||||
statusRemoveListener.removePostsByUser(accountId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
/* 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.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.pkmmte.view.CircularImageView;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
|
||||
class FollowRequestsAdapter extends AccountAdapter {
|
||||
private static final int VIEW_TYPE_FOLLOW_REQUEST = 0;
|
||||
private static final int VIEW_TYPE_FOOTER = 1;
|
||||
|
||||
FollowRequestsAdapter(AccountActionListener accountActionListener) {
|
||||
super(accountActionListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
default:
|
||||
case VIEW_TYPE_FOLLOW_REQUEST: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_follow_request, parent, false);
|
||||
return new FollowRequestViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
return new FooterViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (position < accountList.size()) {
|
||||
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position));
|
||||
holder.setupActionListener(accountActionListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position == accountList.size()) {
|
||||
return VIEW_TYPE_FOOTER;
|
||||
} else {
|
||||
return VIEW_TYPE_FOLLOW_REQUEST;
|
||||
}
|
||||
}
|
||||
|
||||
static class FollowRequestViewHolder extends RecyclerView.ViewHolder {
|
||||
@BindView(R.id.follow_request_avatar) CircularImageView avatar;
|
||||
@BindView(R.id.follow_request_username) TextView username;
|
||||
@BindView(R.id.follow_request_display_name) TextView displayName;
|
||||
@BindView(R.id.follow_request_accept) ImageButton accept;
|
||||
@BindView(R.id.follow_request_reject) ImageButton reject;
|
||||
|
||||
private String id;
|
||||
|
||||
FollowRequestViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
ButterKnife.bind(this, itemView);
|
||||
}
|
||||
|
||||
void setupWithAccount(Account account) {
|
||||
id = account.id;
|
||||
displayName.setText(account.getDisplayName());
|
||||
String format = username.getContext().getString(R.string.status_username_format);
|
||||
String formattedUsername = String.format(format, account.username);
|
||||
username.setText(formattedUsername);
|
||||
Picasso.with(avatar.getContext())
|
||||
.load(account.avatar)
|
||||
.error(R.drawable.avatar_error)
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
.into(avatar);
|
||||
}
|
||||
|
||||
void setupActionListener(final AccountActionListener listener) {
|
||||
accept.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onRespondToFollowRequest(true, id, position);
|
||||
}
|
||||
}
|
||||
});
|
||||
reject.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
listener.onRespondToFollowRequest(false, id, position);
|
||||
}
|
||||
}
|
||||
});
|
||||
avatar.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onViewAccount(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,8 @@ class FooterViewHolder extends RecyclerView.ViewHolder {
|
|||
FooterViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
ProgressBar progressBar = (ProgressBar) itemView.findViewById(R.id.footer_progress_bar);
|
||||
progressBar.setIndeterminate(true);
|
||||
if (progressBar != null) {
|
||||
progressBar.setIndeterminate(true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -19,6 +19,7 @@ import android.support.annotation.Nullable;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
class IOUtils {
|
||||
static void closeQuietly(@Nullable InputStream stream) {
|
||||
|
@ -30,4 +31,14 @@ class IOUtils {
|
|||
// intentionally unhandled
|
||||
}
|
||||
}
|
||||
|
||||
static void closeQuietly(@Nullable OutputStream stream) {
|
||||
try {
|
||||
if (stream != null) {
|
||||
stream.close();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// intentionally unhandled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
82
app/src/main/java/com/keylesspalace/tusky/LinkHelper.java
Normal file
82
app/src/main/java/com/keylesspalace/tusky/LinkHelper.java
Normal file
|
@ -0,0 +1,82 @@
|
|||
/* 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.preference.PreferenceManager;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
|
||||
class LinkHelper {
|
||||
static void setClickableText(TextView view, Spanned content,
|
||||
@Nullable Status.Mention[] mentions,
|
||||
final LinkListener listener) {
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(content);
|
||||
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(view.getContext())
|
||||
.getBoolean("customTabs", true);
|
||||
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class);
|
||||
for (URLSpan span : urlSpans) {
|
||||
int start = builder.getSpanStart(span);
|
||||
int end = builder.getSpanEnd(span);
|
||||
int flags = builder.getSpanFlags(span);
|
||||
CharSequence text = builder.subSequence(start, end);
|
||||
if (text.charAt(0) == '#') {
|
||||
final String tag = text.subSequence(1, text.length()).toString();
|
||||
ClickableSpan newSpan = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
listener.onViewTag(tag);
|
||||
}
|
||||
};
|
||||
builder.removeSpan(span);
|
||||
builder.setSpan(newSpan, start, end, flags);
|
||||
} else if (text.charAt(0) == '@' && mentions != null) {
|
||||
final String accountUsername = text.subSequence(1, text.length()).toString();
|
||||
String id = null;
|
||||
for (Status.Mention mention : mentions) {
|
||||
if (mention.localUsername.equals(accountUsername)) {
|
||||
id = mention.id;
|
||||
}
|
||||
}
|
||||
if (id != null) {
|
||||
final String accountId = id;
|
||||
ClickableSpan newSpan = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
listener.onViewAccount(accountId);
|
||||
}
|
||||
};
|
||||
builder.removeSpan(span);
|
||||
builder.setSpan(newSpan, start, end, flags);
|
||||
}
|
||||
} else if (useCustomTabs) {
|
||||
ClickableSpan newSpan = new CustomTabURLSpan(span.getURL());
|
||||
builder.removeSpan(span);
|
||||
builder.setSpan(newSpan, start, end, flags);
|
||||
}
|
||||
}
|
||||
view.setText(builder);
|
||||
view.setLinksClickable(true);
|
||||
view.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
}
|
21
app/src/main/java/com/keylesspalace/tusky/LinkListener.java
Normal file
21
app/src/main/java/com/keylesspalace/tusky/LinkListener.java
Normal file
|
@ -0,0 +1,21 @@
|
|||
/* 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;
|
||||
|
||||
interface LinkListener {
|
||||
void onViewTag(String tag);
|
||||
void onViewAccount(String id);
|
||||
}
|
|
@ -16,15 +16,17 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.customtabs.CustomTabsIntent;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.View;
|
||||
|
@ -110,26 +112,6 @@ public class LoginActivity extends AppCompatActivity {
|
|||
textView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
}
|
||||
});
|
||||
|
||||
// Apply any updates needed.
|
||||
int versionCode = 1;
|
||||
try {
|
||||
versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;
|
||||
} catch (PackageManager.NameNotFoundException e) {
|
||||
Log.e(TAG, "The app version was not found. " + e.getMessage());
|
||||
}
|
||||
if (preferences.getInt("lastUpdateVersion", 0) != versionCode) {
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
if (versionCode == 14) {
|
||||
/* This version switches the order of scheme and host in the OAuth redirect URI.
|
||||
* But to fix it requires forcing the app to re-authenticate with servers. So, clear
|
||||
* out the stored client id/secret pairs. The only other things that are lost are
|
||||
* "rememberedVisibility", "loggedInUsername", and "loggedInAccountId". */
|
||||
editor.clear();
|
||||
}
|
||||
editor.putInt("lastUpdateVersion", versionCode);
|
||||
editor.apply();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -201,10 +183,10 @@ public class LoginActivity extends AppCompatActivity {
|
|||
AppCredentials credentials = response.body();
|
||||
clientId = credentials.clientId;
|
||||
clientSecret = credentials.clientSecret;
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
editor.putString(domain + "/client_id", clientId);
|
||||
editor.putString(domain + "/client_secret", clientSecret);
|
||||
editor.apply();
|
||||
preferences.edit()
|
||||
.putString(domain + "/client_id", clientId)
|
||||
.putString(domain + "/client_secret", clientSecret)
|
||||
.apply();
|
||||
redirectUserToAuthorizeAndLogin(editText);
|
||||
}
|
||||
|
||||
|
@ -226,7 +208,6 @@ public class LoginActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Chain together the key-value pairs into a query string, for either appending to a URL or
|
||||
* as the content of an HTTP request.
|
||||
|
@ -245,6 +226,36 @@ public class LoginActivity extends AppCompatActivity {
|
|||
return s.toString();
|
||||
}
|
||||
|
||||
private static boolean openInCustomTab(Uri uri, Context context) {
|
||||
boolean lightTheme = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean("lightTheme", false);
|
||||
int toolbarColorRes;
|
||||
if (lightTheme) {
|
||||
toolbarColorRes = R.color.custom_tab_toolbar_light;
|
||||
} else {
|
||||
toolbarColorRes = R.color.custom_tab_toolbar_dark;
|
||||
}
|
||||
int toolbarColor = ContextCompat.getColor(context, toolbarColorRes);
|
||||
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
|
||||
builder.setToolbarColor(toolbarColor);
|
||||
CustomTabsIntent customTabsIntent = builder.build();
|
||||
try {
|
||||
String packageName = CustomTabsHelper.getPackageNameToUse(context);
|
||||
/* If we cant find a package name, it means theres no browser that supports
|
||||
* Chrome Custom Tabs installed. So, we fallback to the webview */
|
||||
if (packageName == null) {
|
||||
return false;
|
||||
} else {
|
||||
customTabsIntent.intent.setPackage(packageName);
|
||||
customTabsIntent.launchUrl(context, uri);
|
||||
}
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w("URLSpan", "Activity was not found for intent, " + customTabsIntent.toString());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void redirectUserToAuthorizeAndLogin(EditText editText) {
|
||||
/* To authorize this app and log in it's necessary to redirect to the domain given,
|
||||
* activity_login there, and the server will redirect back to the app with its response. */
|
||||
|
@ -256,11 +267,14 @@ public class LoginActivity extends AppCompatActivity {
|
|||
parameters.put("response_type", "code");
|
||||
parameters.put("scope", OAUTH_SCOPES);
|
||||
String url = "https://" + domain + endpoint + "?" + toQueryString(parameters);
|
||||
Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
if (viewIntent.resolveActivity(getPackageManager()) != null) {
|
||||
startActivity(viewIntent);
|
||||
} else {
|
||||
editText.setError(getString(R.string.error_no_web_browser_found));
|
||||
Uri uri = Uri.parse(url);
|
||||
if (!openInCustomTab(uri, this)) {
|
||||
Intent viewIntent = new Intent(Intent.ACTION_VIEW, uri);
|
||||
if (viewIntent.resolveActivity(getPackageManager()) != null) {
|
||||
startActivity(viewIntent);
|
||||
} else {
|
||||
editText.setError(getString(R.string.error_no_web_browser_found));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -268,11 +282,11 @@ public class LoginActivity extends AppCompatActivity {
|
|||
protected void onStop() {
|
||||
super.onStop();
|
||||
if (domain != null) {
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
editor.putString("domain", domain);
|
||||
editor.putString("clientId", clientId);
|
||||
editor.putString("clientSecret", clientSecret);
|
||||
editor.apply();
|
||||
preferences.edit()
|
||||
.putString("domain", domain)
|
||||
.putString("clientId", clientId)
|
||||
.putString("clientSecret", clientSecret)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -347,10 +361,14 @@ public class LoginActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private void onLoginSuccess(String accessToken) {
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
editor.putString("domain", domain);
|
||||
editor.putString("accessToken", accessToken);
|
||||
editor.commit();
|
||||
boolean committed = preferences.edit()
|
||||
.putString("domain", domain)
|
||||
.putString("accessToken", accessToken)
|
||||
.commit();
|
||||
if (!committed) {
|
||||
editText.setError(getString(R.string.error_retrieving_oauth_token));
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Typeface;
|
||||
|
@ -24,8 +23,11 @@ import android.graphics.drawable.Drawable;
|
|||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.PersistableBundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
|
@ -64,8 +66,9 @@ import retrofit2.Call;
|
|||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class MainActivity extends BaseActivity {
|
||||
private static final String TAG = "MainActivity"; // logging tag and Volley request tag
|
||||
public class MainActivity extends BaseActivity implements SFragment.OnUserRemovedListener {
|
||||
private static final String TAG = "MainActivity"; // logging tag
|
||||
protected static int COMPOSE_RESULT = 1;
|
||||
|
||||
private String loggedInAccountId;
|
||||
private String loggedInAccountUsername;
|
||||
|
@ -99,7 +102,7 @@ public class MainActivity extends BaseActivity {
|
|||
@Override
|
||||
public void onClick(View v) {
|
||||
Intent intent = new Intent(getApplicationContext(), ComposeActivity.class);
|
||||
startActivity(intent);
|
||||
startActivityForResult(intent, COMPOSE_RESULT);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -184,7 +187,11 @@ public class MainActivity extends BaseActivity {
|
|||
}
|
||||
|
||||
// Setup push notifications
|
||||
if (arePushNotificationsEnabled()) enablePushNotifications();
|
||||
if (arePushNotificationsEnabled()) {
|
||||
enablePushNotifications();
|
||||
} else {
|
||||
disablePushNotifications();
|
||||
}
|
||||
|
||||
composeButton = floatingBtn;
|
||||
}
|
||||
|
@ -193,12 +200,24 @@ public class MainActivity extends BaseActivity {
|
|||
protected void onResume() {
|
||||
super.onResume();
|
||||
|
||||
SharedPreferences notificationPreferences = getApplicationContext().getSharedPreferences("Notifications", MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = notificationPreferences.edit();
|
||||
editor.putString("current", "[]");
|
||||
editor.apply();
|
||||
SharedPreferences notificationPreferences = getApplicationContext()
|
||||
.getSharedPreferences("Notifications", MODE_PRIVATE);
|
||||
notificationPreferences.edit()
|
||||
.putString("current", "[]")
|
||||
.apply();
|
||||
|
||||
((NotificationManager) (getSystemService(NOTIFICATION_SERVICE))).cancel(MyFirebaseMessagingService.NOTIFY_ID);
|
||||
((NotificationManager) (getSystemService(NOTIFICATION_SERVICE)))
|
||||
.cancel(MessagingService.NOTIFY_ID);
|
||||
|
||||
/* After editing a profile, the profile header in the navigation drawer needs to be
|
||||
* refreshed */
|
||||
SharedPreferences preferences = getPrivatePreferences();
|
||||
if (preferences.getBoolean("refreshProfileHeader", false)) {
|
||||
fetchUserInfo();
|
||||
preferences.edit()
|
||||
.putBoolean("refreshProfileHeader", false)
|
||||
.apply();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -254,6 +273,9 @@ public class MainActivity extends BaseActivity {
|
|||
}
|
||||
});
|
||||
|
||||
Drawable muteDrawable = ContextCompat.getDrawable(this, R.drawable.ic_mute_24dp);
|
||||
ThemeUtils.setDrawableTint(this, muteDrawable, R.attr.toolbar_icon_tint);
|
||||
|
||||
drawer = new DrawerBuilder()
|
||||
.withActivity(this)
|
||||
//.withToolbar(toolbar)
|
||||
|
@ -261,12 +283,13 @@ public class MainActivity extends BaseActivity {
|
|||
.withHasStableIds(true)
|
||||
.withSelectedItem(-1)
|
||||
.addDrawerItems(
|
||||
new PrimaryDrawerItem().withIdentifier(0).withName(R.string.action_view_profile).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_person),
|
||||
new PrimaryDrawerItem().withIdentifier(0).withName(getString(R.string.action_edit_profile)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_person),
|
||||
new PrimaryDrawerItem().withIdentifier(1).withName(getString(R.string.action_view_favourites)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_star),
|
||||
new PrimaryDrawerItem().withIdentifier(2).withName(getString(R.string.action_view_blocks)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_block),
|
||||
new PrimaryDrawerItem().withIdentifier(2).withName(getString(R.string.action_view_mutes)).withSelectable(false).withIcon(muteDrawable),
|
||||
new PrimaryDrawerItem().withIdentifier(3).withName(getString(R.string.action_view_blocks)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_block),
|
||||
new DividerDrawerItem(),
|
||||
new SecondaryDrawerItem().withIdentifier(3).withName(getString(R.string.action_view_preferences)).withSelectable(false),
|
||||
new SecondaryDrawerItem().withIdentifier(4).withName(getString(R.string.action_logout)).withSelectable(false)
|
||||
new SecondaryDrawerItem().withIdentifier(4).withName(getString(R.string.action_view_preferences)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_settings),
|
||||
new SecondaryDrawerItem().withIdentifier(5).withName(getString(R.string.action_logout)).withSelectable(false).withIcon(GoogleMaterial.Icon.gmd_exit_to_app)
|
||||
)
|
||||
.withOnDrawerItemClickListener(new Drawer.OnDrawerItemClickListener() {
|
||||
@Override
|
||||
|
@ -275,22 +298,28 @@ public class MainActivity extends BaseActivity {
|
|||
long drawerItemIdentifier = drawerItem.getIdentifier();
|
||||
|
||||
if (drawerItemIdentifier == 0) {
|
||||
if (loggedInAccountId != null) {
|
||||
Intent intent = new Intent(MainActivity.this, AccountActivity.class);
|
||||
intent.putExtra("id", loggedInAccountId);
|
||||
startActivity(intent);
|
||||
}
|
||||
Intent intent = new Intent(MainActivity.this, EditProfileActivity.class);
|
||||
startActivity(intent);
|
||||
} else if (drawerItemIdentifier == 1) {
|
||||
Intent intent = new Intent(MainActivity.this, FavouritesActivity.class);
|
||||
startActivity(intent);
|
||||
} else if (drawerItemIdentifier == 2) {
|
||||
Intent intent = new Intent(MainActivity.this, BlocksActivity.class);
|
||||
Intent intent = new Intent(MainActivity.this, AccountListActivity.class);
|
||||
intent.putExtra("type", AccountListActivity.Type.MUTES);
|
||||
startActivity(intent);
|
||||
} else if (drawerItemIdentifier == 3) {
|
||||
Intent intent = new Intent(MainActivity.this, PreferencesActivity.class);
|
||||
Intent intent = new Intent(MainActivity.this, AccountListActivity.class);
|
||||
intent.putExtra("type", AccountListActivity.Type.BLOCKS);
|
||||
startActivity(intent);
|
||||
} else if (drawerItemIdentifier == 4) {
|
||||
Intent intent = new Intent(MainActivity.this, PreferencesActivity.class);
|
||||
startActivity(intent);
|
||||
} else if (drawerItemIdentifier == 5) {
|
||||
logout();
|
||||
} else if (drawerItemIdentifier == 6) {
|
||||
Intent intent = new Intent(MainActivity.this, AccountListActivity.class);
|
||||
intent.putExtra("type", AccountListActivity.Type.FOLLOW_REQUESTS);
|
||||
startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -303,11 +332,10 @@ public class MainActivity extends BaseActivity {
|
|||
private void logout() {
|
||||
if (arePushNotificationsEnabled()) disablePushNotifications();
|
||||
|
||||
SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
editor.remove("domain");
|
||||
editor.remove("accessToken");
|
||||
editor.apply();
|
||||
getPrivatePreferences().edit()
|
||||
.remove("domain")
|
||||
.remove("accessToken")
|
||||
.apply();
|
||||
|
||||
Intent intent = new Intent(MainActivity.this, LoginActivity.class);
|
||||
startActivity(intent);
|
||||
|
@ -377,9 +405,7 @@ public class MainActivity extends BaseActivity {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onSearchAction(String currentQuery) {
|
||||
|
||||
}
|
||||
public void onSearchAction(String currentQuery) {}
|
||||
});
|
||||
|
||||
searchView.setOnBindSuggestionCallback(new SearchSuggestionsAdapter.OnBindSuggestionCallback() {
|
||||
|
@ -404,8 +430,7 @@ public class MainActivity extends BaseActivity {
|
|||
}
|
||||
|
||||
private void fetchUserInfo() {
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
SharedPreferences preferences = getPrivatePreferences();
|
||||
final String domain = preferences.getString("domain", null);
|
||||
String id = preferences.getString("loggedInAccountId", null);
|
||||
String username = preferences.getString("loggedInAccountUsername", null);
|
||||
|
@ -422,34 +447,7 @@ public class MainActivity extends BaseActivity {
|
|||
onFetchUserInfoFailure(new Exception(response.message()));
|
||||
return;
|
||||
}
|
||||
|
||||
Account me = response.body();
|
||||
ImageView background = headerResult.getHeaderBackgroundView();
|
||||
int backgroundWidth = background.getWidth();
|
||||
int backgroundHeight = background.getHeight();
|
||||
if (backgroundWidth == 0 || backgroundHeight == 0) {
|
||||
/* The header ImageView may not be layed out when the verify credentials call
|
||||
* returns so measure the dimensions and use those. */
|
||||
background.measure(View.MeasureSpec.EXACTLY, View.MeasureSpec.EXACTLY);
|
||||
backgroundWidth = background.getMeasuredWidth();
|
||||
backgroundHeight = background.getMeasuredHeight();
|
||||
}
|
||||
|
||||
Picasso.with(MainActivity.this)
|
||||
.load(me.header)
|
||||
.placeholder(R.drawable.account_header_missing)
|
||||
.resize(backgroundWidth, backgroundHeight)
|
||||
.centerCrop()
|
||||
.into(background);
|
||||
|
||||
headerResult.addProfiles(
|
||||
new ProfileDrawerItem()
|
||||
.withName(me.getDisplayName())
|
||||
.withEmail(String.format("%s@%s", me.username, domain))
|
||||
.withIcon(me.avatar)
|
||||
);
|
||||
|
||||
onFetchUserInfoSuccess(me.id, me.username);
|
||||
onFetchUserInfoSuccess(response.body(), domain);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -459,21 +457,69 @@ public class MainActivity extends BaseActivity {
|
|||
});
|
||||
}
|
||||
|
||||
private void onFetchUserInfoSuccess(String id, String username) {
|
||||
loggedInAccountId = id;
|
||||
loggedInAccountUsername = username;
|
||||
SharedPreferences preferences = getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
SharedPreferences.Editor editor = preferences.edit();
|
||||
editor.putString("loggedInAccountId", loggedInAccountId);
|
||||
editor.putString("loggedInAccountUsername", loggedInAccountUsername);
|
||||
editor.apply();
|
||||
private void onFetchUserInfoSuccess(Account me, String domain) {
|
||||
// Add the header image and avatar from the account, into the navigation drawer header.
|
||||
headerResult.clear();
|
||||
|
||||
ImageView background = headerResult.getHeaderBackgroundView();
|
||||
int backgroundWidth = background.getWidth();
|
||||
int backgroundHeight = background.getHeight();
|
||||
if (backgroundWidth == 0 || backgroundHeight == 0) {
|
||||
/* The header ImageView may not be layed out when the verify credentials call returns so
|
||||
* measure the dimensions and use those. */
|
||||
background.measure(View.MeasureSpec.EXACTLY, View.MeasureSpec.EXACTLY);
|
||||
backgroundWidth = background.getMeasuredWidth();
|
||||
backgroundHeight = background.getMeasuredHeight();
|
||||
}
|
||||
|
||||
Picasso.with(MainActivity.this)
|
||||
.load(me.header)
|
||||
.placeholder(R.drawable.account_header_missing)
|
||||
.resize(backgroundWidth, backgroundHeight)
|
||||
.centerCrop()
|
||||
.into(background);
|
||||
|
||||
headerResult.addProfiles(
|
||||
new ProfileDrawerItem()
|
||||
.withName(me.getDisplayName())
|
||||
.withEmail(String.format("%s@%s", me.username, domain))
|
||||
.withIcon(me.avatar)
|
||||
);
|
||||
|
||||
// Show follow requests in the menu, if this is a locked account.
|
||||
if (me.locked) {
|
||||
PrimaryDrawerItem followRequestsItem = new PrimaryDrawerItem()
|
||||
.withIdentifier(6)
|
||||
.withName(R.string.action_view_follow_requests)
|
||||
.withSelectable(false)
|
||||
.withIcon(GoogleMaterial.Icon.gmd_person_add);
|
||||
drawer.addItemAtPosition(followRequestsItem, 3);
|
||||
}
|
||||
|
||||
// Update the current login information.
|
||||
loggedInAccountId = me.id;
|
||||
loggedInAccountUsername = me.username;
|
||||
getPrivatePreferences().edit()
|
||||
.putString("loggedInAccountId", loggedInAccountId)
|
||||
.putString("loggedInAccountUsername", loggedInAccountUsername)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private void onFetchUserInfoFailure(Exception exception) {
|
||||
Log.e(TAG, "Failed to fetch user info. " + exception.getMessage());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == COMPOSE_RESULT && resultCode == ComposeActivity.RESULT_OK) {
|
||||
TimelinePagerAdapter adapter = (TimelinePagerAdapter) viewPager.getAdapter();
|
||||
if (adapter.getCurrentFragment() instanceof SFragment) {
|
||||
((SFragment) adapter.getCurrentFragment()).onSuccessfulStatus();
|
||||
}
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if(drawer != null && drawer.isDrawerOpen()) {
|
||||
|
@ -485,4 +531,25 @@ public class MainActivity extends BaseActivity {
|
|||
viewPager.setCurrentItem(pageHistory.peek());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
|
||||
@NonNull int[] grantResults) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
TimelinePagerAdapter adapter = (TimelinePagerAdapter) viewPager.getAdapter();
|
||||
for (Fragment fragment : adapter.getRegisteredFragments()) {
|
||||
fragment.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserRemoved(String accountId) {
|
||||
TimelinePagerAdapter adapter = (TimelinePagerAdapter) viewPager.getAdapter();
|
||||
for (Fragment fragment : adapter.getRegisteredFragments()) {
|
||||
if (fragment instanceof StatusRemoveListener) {
|
||||
StatusRemoveListener listener = (StatusRemoveListener) fragment;
|
||||
listener.removePostsByUser(accountId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ import com.keylesspalace.tusky.entity.Account;
|
|||
import com.keylesspalace.tusky.entity.AppCredentials;
|
||||
import com.keylesspalace.tusky.entity.Media;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.keylesspalace.tusky.entity.Profile;
|
||||
import com.keylesspalace.tusky.entity.Relationship;
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
import com.keylesspalace.tusky.entity.StatusContext;
|
||||
|
@ -29,6 +30,7 @@ import java.util.List;
|
|||
import okhttp3.MultipartBody;
|
||||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.Field;
|
||||
import retrofit2.http.FormUrlEncoded;
|
||||
|
@ -115,11 +117,7 @@ public interface MastodonAPI {
|
|||
@GET("api/v1/accounts/verify_credentials")
|
||||
Call<Account> accountVerifyCredentials();
|
||||
@PATCH("api/v1/accounts/update_credentials")
|
||||
Call<Account> accountUpdateCredentials(
|
||||
@Field("display_name") String displayName,
|
||||
@Field("note") String note,
|
||||
@Field("avatar") String avatar,
|
||||
@Field("header") String header);
|
||||
Call<Account> accountUpdateCredentials(@Body Profile profile);
|
||||
@GET("api/v1/accounts/search")
|
||||
Call<List<Account>> searchAccounts(
|
||||
@Query("q") String q,
|
||||
|
|
105
app/src/main/java/com/keylesspalace/tusky/MutesAdapter.java
Normal file
105
app/src/main/java/com/keylesspalace/tusky/MutesAdapter.java
Normal file
|
@ -0,0 +1,105 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.pkmmte.view.CircularImageView;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
|
||||
class MutesAdapter extends AccountAdapter {
|
||||
private static final int VIEW_TYPE_MUTED_USER = 0;
|
||||
private static final int VIEW_TYPE_FOOTER = 1;
|
||||
|
||||
MutesAdapter(AccountActionListener accountActionListener) {
|
||||
super(accountActionListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
default:
|
||||
case VIEW_TYPE_MUTED_USER: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_muted_user, parent, false);
|
||||
return new MutesAdapter.MutedUserViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
return new FooterViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (position < accountList.size()) {
|
||||
MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder;
|
||||
holder.setupWithAccount(accountList.get(position));
|
||||
holder.setupActionListener(accountActionListener, true, position);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (position == accountList.size()) {
|
||||
return VIEW_TYPE_FOOTER;
|
||||
} else {
|
||||
return VIEW_TYPE_MUTED_USER;
|
||||
}
|
||||
}
|
||||
|
||||
static class MutedUserViewHolder extends RecyclerView.ViewHolder {
|
||||
@BindView(R.id.muted_user_avatar) CircularImageView avatar;
|
||||
@BindView(R.id.muted_user_username) TextView username;
|
||||
@BindView(R.id.muted_user_display_name) TextView displayName;
|
||||
@BindView(R.id.muted_user_unmute) ImageButton unmute;
|
||||
|
||||
private String id;
|
||||
|
||||
MutedUserViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
ButterKnife.bind(this, itemView);
|
||||
}
|
||||
|
||||
void setupWithAccount(Account account) {
|
||||
id = account.id;
|
||||
displayName.setText(account.getDisplayName());
|
||||
String format = username.getContext().getString(R.string.status_username_format);
|
||||
String formattedUsername = String.format(format, account.username);
|
||||
username.setText(formattedUsername);
|
||||
Picasso.with(avatar.getContext())
|
||||
.load(account.avatar)
|
||||
.error(R.drawable.avatar_error)
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
.into(avatar);
|
||||
}
|
||||
|
||||
void setupActionListener(final AccountActionListener listener, final boolean muted,
|
||||
final int position) {
|
||||
unmute.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onMute(!muted, id, position);
|
||||
}
|
||||
});
|
||||
avatar.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onViewAccount(id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,83 +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>.
|
||||
*
|
||||
* If you modify this Program, or any covered work, by linking or combining it with Firebase Cloud
|
||||
* Messaging and Firebase Crash Reporting (or a modified version of those libraries), containing
|
||||
* parts covered by the Google APIs Terms of Service, the licensors of this Program grant you
|
||||
* additional permission to convey the resulting work. */
|
||||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.google.firebase.iid.FirebaseInstanceId;
|
||||
import com.google.firebase.iid.FirebaseInstanceIdService;
|
||||
|
||||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import retrofit2.Retrofit;
|
||||
|
||||
public class MyFirebaseInstanceIdService extends FirebaseInstanceIdService {
|
||||
private static final String TAG = "MyFirebaseInstanceIdService";
|
||||
|
||||
private TuskyAPI tuskyAPI;
|
||||
|
||||
protected void createTuskyAPI() {
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl(getString(R.string.tusky_api_url))
|
||||
.client(OkHttpUtils.getCompatibleClient())
|
||||
.build();
|
||||
|
||||
tuskyAPI = retrofit.create(TuskyAPI.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTokenRefresh() {
|
||||
createTuskyAPI();
|
||||
|
||||
String refreshedToken = FirebaseInstanceId.getInstance().getToken();
|
||||
SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
String accessToken = preferences.getString("accessToken", null);
|
||||
String domain = preferences.getString("domain", null);
|
||||
|
||||
if (accessToken != null && domain != null) {
|
||||
tuskyAPI.unregister("https://" + domain, accessToken).enqueue(new Callback<ResponseBody>() {
|
||||
@Override
|
||||
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
|
||||
Log.d(TAG, response.message());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ResponseBody> call, Throwable t) {
|
||||
Log.d(TAG, t.getMessage());
|
||||
}
|
||||
});
|
||||
tuskyAPI.register("https://" + domain, accessToken, refreshedToken).enqueue(new Callback<ResponseBody>() {
|
||||
@Override
|
||||
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
|
||||
Log.d(TAG, response.message());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ResponseBody> call, Throwable t) {
|
||||
Log.d(TAG, t.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,318 +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>.
|
||||
*
|
||||
* If you modify this Program, or any covered work, by linking or combining it with Firebase Cloud
|
||||
* Messaging and Firebase Crash Reporting (or a modified version of those libraries), containing
|
||||
* parts covered by the Google APIs Terms of Service, the licensors of this Program grant you
|
||||
* additional permission to convey the resulting work. */
|
||||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.TaskStackBuilder;
|
||||
import android.text.Spanned;
|
||||
|
||||
import com.google.firebase.messaging.FirebaseMessagingService;
|
||||
import com.google.firebase.messaging.RemoteMessage;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.squareup.picasso.Picasso;
|
||||
import com.squareup.picasso.Target;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
public class MyFirebaseMessagingService extends FirebaseMessagingService {
|
||||
private MastodonAPI mastodonAPI;
|
||||
private static final String TAG = "MyFirebaseMessagingService";
|
||||
public static final int NOTIFY_ID = 666;
|
||||
|
||||
@Override
|
||||
public void onMessageReceived(RemoteMessage remoteMessage) {
|
||||
Log.d(TAG, remoteMessage.getFrom());
|
||||
Log.d(TAG, remoteMessage.toString());
|
||||
|
||||
String notificationId = remoteMessage.getData().get("notification_id");
|
||||
|
||||
if (notificationId == null) {
|
||||
Log.e(TAG, "No notification ID in payload!!");
|
||||
return;
|
||||
}
|
||||
|
||||
Log.d(TAG, notificationId);
|
||||
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
|
||||
getApplicationContext());
|
||||
boolean enabled = preferences.getBoolean("notificationsEnabled", true);
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
createMastodonAPI();
|
||||
|
||||
mastodonAPI.notification(notificationId).enqueue(new Callback<Notification>() {
|
||||
@Override
|
||||
public void onResponse(Call<Notification> call, Response<Notification> response) {
|
||||
if (response.isSuccessful()) {
|
||||
buildNotification(response.body());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Notification> call, Throwable t) {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void createMastodonAPI() {
|
||||
SharedPreferences preferences = getSharedPreferences(getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
final String domain = preferences.getString("domain", null);
|
||||
final String accessToken = preferences.getString("accessToken", null);
|
||||
|
||||
OkHttpClient okHttpClient = OkHttpUtils.getCompatibleClientBuilder()
|
||||
.addInterceptor(new Interceptor() {
|
||||
@Override
|
||||
public okhttp3.Response intercept(Chain chain) throws IOException {
|
||||
Request originalRequest = chain.request();
|
||||
|
||||
Request.Builder builder = originalRequest.newBuilder()
|
||||
.header("Authorization", String.format("Bearer %s", accessToken));
|
||||
|
||||
Request newRequest = builder.build();
|
||||
|
||||
return chain.proceed(newRequest);
|
||||
}
|
||||
})
|
||||
.build();
|
||||
|
||||
Gson gson = new GsonBuilder()
|
||||
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
|
||||
.create();
|
||||
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl("https://" + domain)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(GsonConverterFactory.create(gson))
|
||||
.build();
|
||||
|
||||
mastodonAPI = retrofit.create(MastodonAPI.class);
|
||||
}
|
||||
|
||||
private String truncateWithEllipses(String string, int limit) {
|
||||
if (string.length() < limit) {
|
||||
return string;
|
||||
} else {
|
||||
return string.substring(0, limit - 3) + "...";
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean filterNotification(SharedPreferences preferences,
|
||||
Notification notification) {
|
||||
switch (notification.type) {
|
||||
default:
|
||||
case MENTION: {
|
||||
return preferences.getBoolean("notificationFilterMentions", true);
|
||||
}
|
||||
case FOLLOW: {
|
||||
return preferences.getBoolean("notificationFilterFollows", true);
|
||||
}
|
||||
case REBLOG: {
|
||||
return preferences.getBoolean("notificationFilterReblogs", true);
|
||||
}
|
||||
case FAVOURITE: {
|
||||
return preferences.getBoolean("notificationFilterFavourites", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void buildNotification(Notification body) {
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
final SharedPreferences notificationPreferences = getApplicationContext().getSharedPreferences("Notifications", MODE_PRIVATE);
|
||||
|
||||
if (!filterNotification(preferences, body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String rawCurrentNotifications = notificationPreferences.getString("current", "[]");
|
||||
JSONArray currentNotifications;
|
||||
|
||||
try {
|
||||
currentNotifications = new JSONArray(rawCurrentNotifications);
|
||||
} catch (JSONException e) {
|
||||
currentNotifications = new JSONArray();
|
||||
}
|
||||
|
||||
boolean alreadyContains = false;
|
||||
|
||||
for(int i = 0; i < currentNotifications.length(); i++) {
|
||||
try {
|
||||
if (currentNotifications.getString(i).equals(body.account.displayName)) {
|
||||
alreadyContains = true;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyContains) {
|
||||
currentNotifications.put(body.account.displayName);
|
||||
}
|
||||
|
||||
SharedPreferences.Editor editor = notificationPreferences.edit();
|
||||
editor.putString("current", currentNotifications.toString());
|
||||
editor.commit();
|
||||
|
||||
Intent resultIntent = new Intent(this, MainActivity.class);
|
||||
resultIntent.putExtra("tab_position", 1);
|
||||
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
|
||||
stackBuilder.addParentStack(MainActivity.class);
|
||||
stackBuilder.addNextIntent(resultIntent);
|
||||
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
Intent deleteIntent = new Intent(this, NotificationClearBroadcastReceiver.class);
|
||||
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(this, 0, deleteIntent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
|
||||
final NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setContentIntent(resultPendingIntent)
|
||||
.setDeleteIntent(deletePendingIntent)
|
||||
.setDefaults(0); // So it doesn't ring twice, notify only in Target callback
|
||||
|
||||
if (currentNotifications.length() == 1) {
|
||||
builder.setContentTitle(titleForType(body))
|
||||
.setContentText(truncateWithEllipses(bodyForType(body), 40));
|
||||
|
||||
Target mTarget = new Target() {
|
||||
@Override
|
||||
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
|
||||
builder.setLargeIcon(bitmap);
|
||||
|
||||
setupPreferences(preferences, builder);
|
||||
|
||||
((NotificationManager) (getSystemService(NOTIFICATION_SERVICE))).notify(NOTIFY_ID, builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBitmapFailed(Drawable errorDrawable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareLoad(Drawable placeHolderDrawable) {
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
Picasso.with(this)
|
||||
.load(body.account.avatar)
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
.transform(new RoundedTransformation(7, 0))
|
||||
.into(mTarget);
|
||||
} else {
|
||||
setupPreferences(preferences, builder);
|
||||
|
||||
try {
|
||||
builder.setContentTitle(String.format(getString(R.string.notification_title_summary), currentNotifications.length()))
|
||||
.setContentText(truncateWithEllipses(joinNames(currentNotifications), 40));
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
builder.setVisibility(android.app.Notification.VISIBILITY_PRIVATE);
|
||||
builder.setCategory(android.app.Notification.CATEGORY_SOCIAL);
|
||||
}
|
||||
|
||||
((NotificationManager) (getSystemService(NOTIFICATION_SERVICE))).notify(NOTIFY_ID, builder.build());
|
||||
}
|
||||
|
||||
private void setupPreferences(SharedPreferences preferences, NotificationCompat.Builder builder) {
|
||||
if (preferences.getBoolean("notificationAlertSound", true)) {
|
||||
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
|
||||
}
|
||||
|
||||
if (preferences.getBoolean("notificationAlertVibrate", false)) {
|
||||
builder.setVibrate(new long[] { 500, 500 });
|
||||
}
|
||||
|
||||
if (preferences.getBoolean("notificationAlertLight", false)) {
|
||||
builder.setLights(0xFF00FF8F, 300, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
private String joinNames(JSONArray array) throws JSONException {
|
||||
if (array.length() > 3) {
|
||||
return String.format(getString(R.string.notification_summary_large), array.get(0), array.get(1), array.get(2), array.length() - 3);
|
||||
} else if (array.length() == 3) {
|
||||
return String.format(getString(R.string.notification_summary_medium), array.get(0), array.get(1), array.get(2));
|
||||
} else if (array.length() == 2) {
|
||||
return String.format(getString(R.string.notification_summary_small), array.get(0), array.get(1));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String titleForType(Notification notification) {
|
||||
switch (notification.type) {
|
||||
case MENTION:
|
||||
return String.format(getString(R.string.notification_mention_format), notification.account.getDisplayName());
|
||||
case FOLLOW:
|
||||
return String.format(getString(R.string.notification_follow_format), notification.account.getDisplayName());
|
||||
case FAVOURITE:
|
||||
return String.format(getString(R.string.notification_favourite_format), notification.account.getDisplayName());
|
||||
case REBLOG:
|
||||
return String.format(getString(R.string.notification_reblog_format), notification.account.getDisplayName());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private String bodyForType(Notification notification) {
|
||||
switch (notification.type) {
|
||||
case FOLLOW:
|
||||
return notification.account.username;
|
||||
case MENTION:
|
||||
case FAVOURITE:
|
||||
case REBLOG:
|
||||
return notification.status.content.toString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
224
app/src/main/java/com/keylesspalace/tusky/NotificationMaker.java
Normal file
224
app/src/main/java/com/keylesspalace/tusky/NotificationMaker.java
Normal file
|
@ -0,0 +1,224 @@
|
|||
/* 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.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.provider.Settings;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.support.v4.app.TaskStackBuilder;
|
||||
|
||||
import com.keylesspalace.tusky.entity.Notification;
|
||||
import com.squareup.picasso.Picasso;
|
||||
import com.squareup.picasso.Target;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
class NotificationMaker {
|
||||
static void make(final Context context, final int notifyId, Notification body) {
|
||||
final SharedPreferences preferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final SharedPreferences notificationPreferences = context.getSharedPreferences(
|
||||
"Notifications", Context.MODE_PRIVATE);
|
||||
|
||||
if (!filterNotification(preferences, body)) {
|
||||
return;
|
||||
}
|
||||
|
||||
String rawCurrentNotifications = notificationPreferences.getString("current", "[]");
|
||||
JSONArray currentNotifications;
|
||||
|
||||
try {
|
||||
currentNotifications = new JSONArray(rawCurrentNotifications);
|
||||
} catch (JSONException e) {
|
||||
currentNotifications = new JSONArray();
|
||||
}
|
||||
|
||||
boolean alreadyContains = false;
|
||||
|
||||
for(int i = 0; i < currentNotifications.length(); i++) {
|
||||
try {
|
||||
if (currentNotifications.getString(i).equals(body.account.getDisplayName())) {
|
||||
alreadyContains = true;
|
||||
}
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (!alreadyContains) {
|
||||
currentNotifications.put(body.account.getDisplayName());
|
||||
}
|
||||
|
||||
notificationPreferences.edit()
|
||||
.putString("current", currentNotifications.toString())
|
||||
.commit();
|
||||
|
||||
Intent resultIntent = new Intent(context, MainActivity.class);
|
||||
resultIntent.putExtra("tab_position", 1);
|
||||
TaskStackBuilder stackBuilder = TaskStackBuilder.create(context);
|
||||
stackBuilder.addParentStack(MainActivity.class);
|
||||
stackBuilder.addNextIntent(resultIntent);
|
||||
PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
|
||||
Intent deleteIntent = new Intent(context, NotificationClearBroadcastReceiver.class);
|
||||
PendingIntent deletePendingIntent = PendingIntent.getBroadcast(context, 0, deleteIntent, PendingIntent.FLAG_CANCEL_CURRENT);
|
||||
|
||||
final NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
|
||||
.setSmallIcon(R.drawable.ic_notify)
|
||||
.setContentIntent(resultPendingIntent)
|
||||
.setDeleteIntent(deletePendingIntent)
|
||||
.setDefaults(0); // So it doesn't ring twice, notify only in Target callback
|
||||
|
||||
if (currentNotifications.length() == 1) {
|
||||
builder.setContentTitle(titleForType(context, body))
|
||||
.setContentText(truncateWithEllipses(bodyForType(body), 40));
|
||||
|
||||
Target mTarget = new Target() {
|
||||
@Override
|
||||
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
|
||||
builder.setLargeIcon(bitmap);
|
||||
|
||||
setupPreferences(preferences, builder);
|
||||
|
||||
((NotificationManager) (context.getSystemService(Context.NOTIFICATION_SERVICE)))
|
||||
.notify(notifyId, builder.build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBitmapFailed(Drawable errorDrawable) {}
|
||||
|
||||
@Override
|
||||
public void onPrepareLoad(Drawable placeHolderDrawable) {}
|
||||
};
|
||||
|
||||
Picasso.with(context)
|
||||
.load(body.account.avatar)
|
||||
.placeholder(R.drawable.avatar_default)
|
||||
.transform(new RoundedTransformation(7, 0))
|
||||
.into(mTarget);
|
||||
} else {
|
||||
setupPreferences(preferences, builder);
|
||||
try {
|
||||
builder.setContentTitle(String.format(context.getString(R.string.notification_title_summary), currentNotifications.length()))
|
||||
.setContentText(truncateWithEllipses(joinNames(context, currentNotifications), 40));
|
||||
} catch (JSONException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
builder.setVisibility(android.app.Notification.VISIBILITY_PRIVATE);
|
||||
builder.setCategory(android.app.Notification.CATEGORY_SOCIAL);
|
||||
}
|
||||
|
||||
((NotificationManager) (context.getSystemService(Context.NOTIFICATION_SERVICE)))
|
||||
.notify(notifyId, builder.build());
|
||||
}
|
||||
|
||||
private static boolean filterNotification(SharedPreferences preferences,
|
||||
Notification notification) {
|
||||
switch (notification.type) {
|
||||
default:
|
||||
case MENTION: {
|
||||
return preferences.getBoolean("notificationFilterMentions", true);
|
||||
}
|
||||
case FOLLOW: {
|
||||
return preferences.getBoolean("notificationFilterFollows", true);
|
||||
}
|
||||
case REBLOG: {
|
||||
return preferences.getBoolean("notificationFilterReblogs", true);
|
||||
}
|
||||
case FAVOURITE: {
|
||||
return preferences.getBoolean("notificationFilterFavourites", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String truncateWithEllipses(String string, int limit) {
|
||||
if (string.length() < limit) {
|
||||
return string;
|
||||
} else {
|
||||
return string.substring(0, limit - 3) + "...";
|
||||
}
|
||||
}
|
||||
|
||||
private static void setupPreferences(SharedPreferences preferences,
|
||||
NotificationCompat.Builder builder) {
|
||||
if (preferences.getBoolean("notificationAlertSound", true)) {
|
||||
builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI);
|
||||
}
|
||||
|
||||
if (preferences.getBoolean("notificationAlertVibrate", false)) {
|
||||
builder.setVibrate(new long[] { 500, 500 });
|
||||
}
|
||||
|
||||
if (preferences.getBoolean("notificationAlertLight", false)) {
|
||||
builder.setLights(0xFF00FF8F, 300, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String joinNames(Context context, JSONArray array) throws JSONException {
|
||||
if (array.length() > 3) {
|
||||
return String.format(context.getString(R.string.notification_summary_large), array.get(0), array.get(1), array.get(2), array.length() - 3);
|
||||
} else if (array.length() == 3) {
|
||||
return String.format(context.getString(R.string.notification_summary_medium), array.get(0), array.get(1), array.get(2));
|
||||
} else if (array.length() == 2) {
|
||||
return String.format(context.getString(R.string.notification_summary_small), array.get(0), array.get(1));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String titleForType(Context context, Notification notification) {
|
||||
switch (notification.type) {
|
||||
case MENTION:
|
||||
return String.format(context.getString(R.string.notification_mention_format), notification.account.getDisplayName());
|
||||
case FOLLOW:
|
||||
return String.format(context.getString(R.string.notification_follow_format), notification.account.getDisplayName());
|
||||
case FAVOURITE:
|
||||
return String.format(context.getString(R.string.notification_favourite_format), notification.account.getDisplayName());
|
||||
case REBLOG:
|
||||
return String.format(context.getString(R.string.notification_reblog_format), notification.account.getDisplayName());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String bodyForType(Notification notification) {
|
||||
switch (notification.type) {
|
||||
case FOLLOW:
|
||||
return notification.account.username;
|
||||
case MENTION:
|
||||
case FAVOURITE:
|
||||
case REBLOG:
|
||||
return notification.status.content.toString();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -34,6 +34,7 @@ import com.keylesspalace.tusky.entity.Status;
|
|||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
|
||||
|
@ -42,9 +43,16 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
|
|||
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2;
|
||||
private static final int VIEW_TYPE_FOLLOW = 3;
|
||||
|
||||
enum FooterState {
|
||||
EMPTY,
|
||||
END,
|
||||
LOADING
|
||||
}
|
||||
|
||||
private List<Notification> notifications;
|
||||
private StatusActionListener statusListener;
|
||||
private NotificationActionListener notificationActionListener;
|
||||
private FooterState footerState = FooterState.END;
|
||||
|
||||
NotificationsAdapter(StatusActionListener statusListener,
|
||||
NotificationActionListener notificationActionListener) {
|
||||
|
@ -54,6 +62,15 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
|
|||
this.notificationActionListener = notificationActionListener;
|
||||
}
|
||||
|
||||
|
||||
void setFooterState(FooterState newFooterState) {
|
||||
FooterState oldValue = footerState;
|
||||
footerState = newFooterState;
|
||||
if (footerState != oldValue) {
|
||||
notifyItemChanged(notifications.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
|
@ -64,8 +81,24 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
|
|||
return new StatusViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
View view;
|
||||
switch (footerState) {
|
||||
default:
|
||||
case LOADING:
|
||||
view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer, parent, false);
|
||||
break;
|
||||
case END: {
|
||||
view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer_end, parent, false);
|
||||
break;
|
||||
}
|
||||
case EMPTY: {
|
||||
view = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.item_footer_empty, parent, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new FooterViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_STATUS_NOTIFICATION: {
|
||||
|
@ -178,6 +211,18 @@ class NotificationsAdapter extends RecyclerView.Adapter implements AdapterItemRe
|
|||
notifyItemChanged(position);
|
||||
}
|
||||
|
||||
public void removeAllByAccountId(String id) {
|
||||
for (int i = 0; i < notifications.size();) {
|
||||
Notification notification = notifications.get(i);
|
||||
if (id.equals(notification.account.id)) {
|
||||
notifications.remove(i);
|
||||
notifyItemRemoved(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface NotificationActionListener {
|
||||
void onViewAccount(String id);
|
||||
}
|
||||
|
|
|
@ -19,7 +19,9 @@ import android.content.Context;
|
|||
import android.content.SharedPreferences;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.widget.DividerItemDecoration;
|
||||
|
@ -39,16 +41,18 @@ import retrofit2.Callback;
|
|||
import retrofit2.Response;
|
||||
|
||||
public class NotificationsFragment extends SFragment implements
|
||||
SwipeRefreshLayout.OnRefreshListener, StatusActionListener,
|
||||
NotificationsAdapter.NotificationActionListener {
|
||||
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, StatusRemoveListener,
|
||||
NotificationsAdapter.NotificationActionListener, SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String TAG = "Notifications"; // logging tag
|
||||
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private LinearLayoutManager layoutManager;
|
||||
private RecyclerView recyclerView;
|
||||
private EndlessOnScrollListener scrollListener;
|
||||
private NotificationsAdapter adapter;
|
||||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||
private Call<List<Notification>> listCall;
|
||||
private boolean hideFab;
|
||||
|
||||
public static NotificationsFragment newInstance() {
|
||||
NotificationsFragment fragment = new NotificationsFragment();
|
||||
|
@ -57,15 +61,10 @@ public class NotificationsFragment extends SFragment implements
|
|||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_timeline, container, false);
|
||||
|
||||
// Setup the SwipeRefreshLayout.
|
||||
|
@ -73,7 +72,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh_layout);
|
||||
swipeRefreshLayout.setOnRefreshListener(this);
|
||||
// Setup the RecyclerView.
|
||||
RecyclerView recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
|
||||
recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
|
||||
recyclerView.setHasFixedSize(true);
|
||||
layoutManager = new LinearLayoutManager(context);
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
|
@ -83,19 +82,7 @@ public class NotificationsFragment extends SFragment implements
|
|||
R.drawable.status_divider_dark);
|
||||
divider.setDrawable(drawable);
|
||||
recyclerView.addItemDecoration(divider);
|
||||
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
||||
@Override
|
||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
||||
NotificationsAdapter adapter = (NotificationsAdapter) view.getAdapter();
|
||||
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
|
||||
if (notification != null) {
|
||||
sendFetchNotificationsRequest(notification.id, null);
|
||||
} else {
|
||||
sendFetchNotificationsRequest();
|
||||
}
|
||||
}
|
||||
};
|
||||
recyclerView.addOnScrollListener(scrollListener);
|
||||
|
||||
adapter = new NotificationsAdapter(this, this);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
|
@ -118,9 +105,48 @@ public class NotificationsFragment extends SFragment implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
sendFetchNotificationsRequest();
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
/* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't
|
||||
* guaranteed to be set until then.
|
||||
* Use a modified scroll listener that both loads more notifications as it goes, and hides
|
||||
* the compose button on down-scroll. */
|
||||
MainActivity activity = (MainActivity) getActivity();
|
||||
final FloatingActionButton composeButton = activity.composeButton;
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
|
||||
activity);
|
||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
||||
hideFab = preferences.getBoolean("fabHide", false);
|
||||
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView view, int dx, int dy) {
|
||||
super.onScrolled(view, dx, dy);
|
||||
|
||||
if (hideFab) {
|
||||
if (dy > 0 && composeButton.isShown()) {
|
||||
composeButton.hide(); // hides the button if we're scrolling down
|
||||
} else if (dy < 0 && !composeButton.isShown()) {
|
||||
composeButton.show(); // shows it if we are scrolling up
|
||||
}
|
||||
} else if (!composeButton.isShown()) {
|
||||
composeButton.show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
||||
NotificationsAdapter adapter = (NotificationsAdapter) view.getAdapter();
|
||||
Notification notification = adapter.getItem(adapter.getItemCount() - 2);
|
||||
if (notification != null) {
|
||||
sendFetchNotificationsRequest(notification.id, null);
|
||||
} else {
|
||||
sendFetchNotificationsRequest();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
recyclerView.addOnScrollListener(scrollListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -142,9 +168,11 @@ public class NotificationsFragment extends SFragment implements
|
|||
}
|
||||
|
||||
private void sendFetchNotificationsRequest(final String fromId, String uptoId) {
|
||||
MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI;
|
||||
if (fromId != null || adapter.getItemCount() <= 1) {
|
||||
adapter.setFooterState(NotificationsAdapter.FooterState.LOADING);
|
||||
}
|
||||
|
||||
listCall = api.notifications(fromId, uptoId, null);
|
||||
listCall = mastodonAPI.notifications(fromId, uptoId, null);
|
||||
|
||||
listCall.enqueue(new Callback<List<Notification>>() {
|
||||
@Override
|
||||
|
@ -168,6 +196,10 @@ public class NotificationsFragment extends SFragment implements
|
|||
sendFetchNotificationsRequest(null, null);
|
||||
}
|
||||
|
||||
public void removePostsByUser(String accountId) {
|
||||
adapter.removeAllByAccountId(accountId);
|
||||
}
|
||||
|
||||
private static boolean findNotification(List<Notification> notifications, String id) {
|
||||
for (Notification notification : notifications) {
|
||||
if (notification.id.equals(id)) {
|
||||
|
@ -192,6 +224,11 @@ public class NotificationsFragment extends SFragment implements
|
|||
} else {
|
||||
adapter.update(notifications);
|
||||
}
|
||||
if (notifications.size() == 0 && adapter.getItemCount() == 1) {
|
||||
adapter.setFooterState(NotificationsAdapter.FooterState.EMPTY);
|
||||
} else if (fromId != null) {
|
||||
adapter.setFooterState(NotificationsAdapter.FooterState.END);
|
||||
}
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
|
@ -245,4 +282,11 @@ public class NotificationsFragment extends SFragment implements
|
|||
public void onViewAccount(String id) {
|
||||
super.viewAccount(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if(key.equals("fabHide")) {
|
||||
hideFab = sharedPreferences.getBoolean("fabHide", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,9 +37,12 @@ import javax.net.ssl.TrustManagerFactory;
|
|||
import javax.net.ssl.X509TrustManager;
|
||||
|
||||
import okhttp3.ConnectionSpec;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
|
||||
class OkHttpUtils {
|
||||
public class OkHttpUtils {
|
||||
static final String TAG = "OkHttpUtils"; // logging tag
|
||||
|
||||
/**
|
||||
|
@ -55,8 +58,7 @@ class OkHttpUtils {
|
|||
* TLS 1.1 and 1.2 have to be manually enabled on API levels 16-20.
|
||||
*/
|
||||
@NonNull
|
||||
static OkHttpClient.Builder getCompatibleClientBuilder() {
|
||||
|
||||
public static OkHttpClient.Builder getCompatibleClientBuilder() {
|
||||
ConnectionSpec fallback = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
|
||||
.allEnabledCipherSuites()
|
||||
.supportsTlsExtensions(true)
|
||||
|
@ -69,16 +71,37 @@ class OkHttpUtils {
|
|||
specList.add(ConnectionSpec.CLEARTEXT);
|
||||
|
||||
OkHttpClient.Builder builder = new OkHttpClient.Builder()
|
||||
.addInterceptor(getUserAgentInterceptor())
|
||||
.connectionSpecs(specList);
|
||||
|
||||
return enableHigherTlsOnPreLollipop(builder);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
static OkHttpClient getCompatibleClient() {
|
||||
public static OkHttpClient getCompatibleClient() {
|
||||
return getCompatibleClientBuilder().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a custom User-Agent that contains Tusky & Android Version to all requests
|
||||
* Example:
|
||||
* User-Agent: Tusky/1.1.2 Android/5.0.2
|
||||
*/
|
||||
@NonNull
|
||||
private static Interceptor getUserAgentInterceptor() {
|
||||
return new Interceptor() {
|
||||
@Override
|
||||
public Response intercept(Chain chain) throws IOException {
|
||||
Request originalRequest = chain.request();
|
||||
Request requestWithUserAgent = originalRequest.newBuilder()
|
||||
.header("User-Agent", "Tusky/"+BuildConfig.VERSION_NAME+" Android/"+Build.VERSION.RELEASE)
|
||||
.build();
|
||||
return chain.proceed(requestWithUserAgent);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Android version Nougat has a regression where elliptic curve cipher suites are supported, but
|
||||
* only the curve secp256r1 is allowed. So, first it's best to just disable all elliptic
|
||||
|
@ -194,7 +217,7 @@ class OkHttpUtils {
|
|||
|
||||
@Override
|
||||
public Socket createSocket(InetAddress address, int port, InetAddress localAddress,
|
||||
int localPort) throws IOException {
|
||||
int localPort) throws IOException {
|
||||
return patch(delegate.createSocket(address, port, localAddress, localPort));
|
||||
}
|
||||
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
|
@ -37,6 +36,7 @@ import java.util.List;
|
|||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
/* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an
|
||||
* awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature
|
||||
|
@ -44,22 +44,32 @@ import retrofit2.Callback;
|
|||
* adapters. I feel like the profile pages and thread viewer, which I haven't made yet, will also
|
||||
* overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear
|
||||
* up what needs to be where. */
|
||||
public class SFragment extends BaseFragment {
|
||||
public abstract class SFragment extends BaseFragment {
|
||||
interface OnUserRemovedListener {
|
||||
void onUserRemoved(String accountId);
|
||||
}
|
||||
|
||||
protected String loggedInAccountId;
|
||||
protected String loggedInUsername;
|
||||
protected MastodonAPI mastodonAPI;
|
||||
protected OnUserRemovedListener userRemovedListener;
|
||||
protected static int COMPOSE_RESULT = 1;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
SharedPreferences preferences = getContext().getSharedPreferences(
|
||||
getString(R.string.preferences_file_key), Context.MODE_PRIVATE);
|
||||
SharedPreferences preferences = getPrivatePreferences();
|
||||
loggedInAccountId = preferences.getString("loggedInAccountId", null);
|
||||
loggedInUsername = preferences.getString("loggedInAccountUsername", null);
|
||||
}
|
||||
|
||||
public MastodonAPI getApi() {
|
||||
return ((BaseActivity) getActivity()).mastodonAPI;
|
||||
@Override
|
||||
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
BaseActivity activity = (BaseActivity) getActivity();
|
||||
mastodonAPI = activity.mastodonAPI;
|
||||
userRemovedListener = (OnUserRemovedListener) activity;
|
||||
}
|
||||
|
||||
protected void reply(Status status) {
|
||||
|
@ -79,11 +89,22 @@ public class SFragment extends BaseFragment {
|
|||
intent.putExtra("reply_visibility", replyVisibility);
|
||||
intent.putExtra("content_warning", contentWarning);
|
||||
intent.putExtra("mentioned_usernames", mentionedUsernames.toArray(new String[0]));
|
||||
startActivity(intent);
|
||||
startActivityForResult(intent, COMPOSE_RESULT);
|
||||
}
|
||||
|
||||
public void onSuccessfulStatus() {}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (requestCode == COMPOSE_RESULT && resultCode == ComposeActivity.RESULT_OK) {
|
||||
onSuccessfulStatus();
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
protected void reblog(final Status status, final boolean reblog,
|
||||
final RecyclerView.Adapter adapter, final int position) {
|
||||
final RecyclerView.Adapter adapter, final int position) {
|
||||
String id = status.getActionableId();
|
||||
|
||||
Callback<Status> cb = new Callback<Status>() {
|
||||
|
@ -101,16 +122,14 @@ public class SFragment extends BaseFragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Status> call, Throwable t) {
|
||||
|
||||
}
|
||||
public void onFailure(Call<Status> call, Throwable t) {}
|
||||
};
|
||||
|
||||
Call<Status> call;
|
||||
if (reblog) {
|
||||
call = getApi().reblogStatus(id);
|
||||
call = mastodonAPI.reblogStatus(id);
|
||||
} else {
|
||||
call = getApi().unreblogStatus(id);
|
||||
call = mastodonAPI.unreblogStatus(id);
|
||||
}
|
||||
call.enqueue(cb);
|
||||
callList.add(call);
|
||||
|
@ -135,55 +154,59 @@ public class SFragment extends BaseFragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Status> call, Throwable t) {
|
||||
|
||||
}
|
||||
public void onFailure(Call<Status> call, Throwable t) {}
|
||||
};
|
||||
|
||||
Call<Status> call;
|
||||
if (favourite) {
|
||||
call = getApi().favouriteStatus(id);
|
||||
call = mastodonAPI.favouriteStatus(id);
|
||||
} else {
|
||||
call = getApi().unfavouriteStatus(id);
|
||||
call = mastodonAPI.unfavouriteStatus(id);
|
||||
}
|
||||
call.enqueue(cb);
|
||||
callList.add(call);
|
||||
}
|
||||
|
||||
private void block(String id) {
|
||||
Call<Relationship> call = getApi().blockAccount(id);
|
||||
private void mute(String id) {
|
||||
Call<Relationship> call = mastodonAPI.muteAccount(id);
|
||||
call.enqueue(new Callback<Relationship>() {
|
||||
@Override
|
||||
public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {
|
||||
|
||||
}
|
||||
public void onResponse(Call<Relationship> call, Response<Relationship> response) {}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Relationship> call, Throwable t) {
|
||||
|
||||
}
|
||||
public void onFailure(Call<Relationship> call, Throwable t) {}
|
||||
});
|
||||
callList.add(call);
|
||||
userRemovedListener.onUserRemoved(id);
|
||||
}
|
||||
|
||||
private void block(String id) {
|
||||
Call<Relationship> call = mastodonAPI.blockAccount(id);
|
||||
call.enqueue(new Callback<Relationship>() {
|
||||
@Override
|
||||
public void onResponse(Call<Relationship> call, retrofit2.Response<Relationship> response) {}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Relationship> call, Throwable t) {}
|
||||
});
|
||||
callList.add(call);
|
||||
userRemovedListener.onUserRemoved(id);
|
||||
}
|
||||
|
||||
private void delete(String id) {
|
||||
Call<ResponseBody> call = getApi().deleteStatus(id);
|
||||
Call<ResponseBody> call = mastodonAPI.deleteStatus(id);
|
||||
call.enqueue(new Callback<ResponseBody>() {
|
||||
@Override
|
||||
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {
|
||||
|
||||
}
|
||||
public void onResponse(Call<ResponseBody> call, retrofit2.Response<ResponseBody> response) {}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ResponseBody> call, Throwable t) {
|
||||
|
||||
}
|
||||
public void onFailure(Call<ResponseBody> call, Throwable t) {}
|
||||
});
|
||||
callList.add(call);
|
||||
}
|
||||
|
||||
protected void more(Status status, View view, final AdapterItemRemover adapter,
|
||||
final int position) {
|
||||
protected void more(final Status status, View view, final AdapterItemRemover adapter,
|
||||
final int position) {
|
||||
final String id = status.getActionableId();
|
||||
final String accountId = status.getActionableStatus().account.id;
|
||||
final String accountUsename = status.getActionableStatus().account.username;
|
||||
|
@ -201,12 +224,29 @@ public class SFragment extends BaseFragment {
|
|||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.status_share: {
|
||||
case R.id.status_share_content: {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append(status.account.username);
|
||||
sb.append(" - ");
|
||||
sb.append(status.content.toString());
|
||||
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.setAction(Intent.ACTION_SEND);
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, sb.toString());
|
||||
sendIntent.setType("text/plain");
|
||||
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_content_to)));
|
||||
return true;
|
||||
}
|
||||
case R.id.status_share_link: {
|
||||
Intent sendIntent = new Intent();
|
||||
sendIntent.setAction(Intent.ACTION_SEND);
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl);
|
||||
sendIntent.setType("text/plain");
|
||||
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_to)));
|
||||
startActivity(Intent.createChooser(sendIntent, getResources().getText(R.string.send_status_link_to)));
|
||||
return true;
|
||||
}
|
||||
case R.id.status_mute: {
|
||||
mute(accountId);
|
||||
return true;
|
||||
}
|
||||
case R.id.status_block: {
|
||||
|
|
|
@ -25,7 +25,7 @@ import com.google.gson.JsonParseException;
|
|||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
class SpannedTypeAdapter implements JsonDeserializer<Spanned> {
|
||||
public class SpannedTypeAdapter implements JsonDeserializer<Spanned> {
|
||||
@Override
|
||||
public Spanned deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
|
||||
return HtmlUtils.fromHtml(Emojione.shortnameToUnicode(json.getAsString(), false));
|
||||
|
|
|
@ -19,13 +19,11 @@ import android.view.View;
|
|||
|
||||
import com.keylesspalace.tusky.entity.Status;
|
||||
|
||||
interface StatusActionListener {
|
||||
interface StatusActionListener extends LinkListener {
|
||||
void onReply(int position);
|
||||
void onReblog(final boolean reblog, final int position);
|
||||
void onFavourite(final boolean favourite, final int position);
|
||||
void onMore(View view, final int position);
|
||||
void onViewMedia(String url, Status.MediaAttachment.Type type);
|
||||
void onViewThread(int position);
|
||||
void onViewTag(String tag);
|
||||
void onViewAccount(String id);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
/* 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;
|
||||
|
||||
interface StatusRemoveListener {
|
||||
void removePostsByUser(String accountId);
|
||||
}
|
|
@ -16,14 +16,9 @@
|
|||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Context;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.view.View;
|
||||
import android.widget.CompoundButton;
|
||||
import android.widget.ImageButton;
|
||||
|
@ -102,57 +97,10 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
private void setContent(Spanned content, Status.Mention[] mentions,
|
||||
final StatusActionListener listener) {
|
||||
StatusActionListener listener) {
|
||||
/* Redirect URLSpan's in the status content to the listener for viewing tag pages and
|
||||
* account pages. */
|
||||
SpannableStringBuilder builder = new SpannableStringBuilder(content);
|
||||
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(container.getContext()).getBoolean("customTabs", true);
|
||||
URLSpan[] urlSpans = content.getSpans(0, content.length(), URLSpan.class);
|
||||
for (URLSpan span : urlSpans) {
|
||||
int start = builder.getSpanStart(span);
|
||||
int end = builder.getSpanEnd(span);
|
||||
int flags = builder.getSpanFlags(span);
|
||||
CharSequence text = builder.subSequence(start, end);
|
||||
if (text.charAt(0) == '#') {
|
||||
final String tag = text.subSequence(1, text.length()).toString();
|
||||
ClickableSpan newSpan = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
listener.onViewTag(tag);
|
||||
}
|
||||
};
|
||||
builder.removeSpan(span);
|
||||
builder.setSpan(newSpan, start, end, flags);
|
||||
} else if (text.charAt(0) == '@') {
|
||||
final String accountUsername = text.subSequence(1, text.length()).toString();
|
||||
String id = null;
|
||||
for (Status.Mention mention: mentions) {
|
||||
if (mention.username.equals(accountUsername)) {
|
||||
id = mention.id;
|
||||
}
|
||||
}
|
||||
if (id != null) {
|
||||
final String accountId = id;
|
||||
ClickableSpan newSpan = new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(View widget) {
|
||||
listener.onViewAccount(accountId);
|
||||
}
|
||||
};
|
||||
builder.removeSpan(span);
|
||||
builder.setSpan(newSpan, start, end, flags);
|
||||
}
|
||||
} else if (useCustomTabs) {
|
||||
ClickableSpan newSpan = new CustomTabURLSpan(span.getURL());
|
||||
builder.removeSpan(span);
|
||||
builder.setSpan(newSpan, start, end, flags);
|
||||
}
|
||||
}
|
||||
// Set the contents.
|
||||
this.content.setText(builder);
|
||||
// Make links clickable.
|
||||
this.content.setLinksClickable(true);
|
||||
this.content.setMovementMethod(LinkMovementMethod.getInstance());
|
||||
LinkHelper.setClickableText(this.content, content, mentions, listener);
|
||||
}
|
||||
|
||||
private void setAvatar(String url) {
|
||||
|
@ -230,7 +178,7 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
|
||||
private void setMediaPreviews(final Status.MediaAttachment[] attachments,
|
||||
boolean sensitive, final StatusActionListener listener) {
|
||||
boolean sensitive, final StatusActionListener listener) {
|
||||
final ImageView[] previews = {
|
||||
mediaPreview0,
|
||||
mediaPreview1,
|
||||
|
@ -249,20 +197,32 @@ class StatusViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
previews[i].setVisibility(View.VISIBLE);
|
||||
|
||||
Picasso.with(context)
|
||||
.load(previewUrl)
|
||||
.placeholder(mediaPreviewUnloadedId)
|
||||
.into(previews[i]);
|
||||
if(previewUrl == null || previewUrl.isEmpty()) {
|
||||
Picasso.with(context)
|
||||
.load(mediaPreviewUnloadedId)
|
||||
.into(previews[i]);
|
||||
} else {
|
||||
Picasso.with(context)
|
||||
.load(previewUrl)
|
||||
.placeholder(mediaPreviewUnloadedId)
|
||||
.into(previews[i]);
|
||||
}
|
||||
|
||||
final String url = attachments[i].url;
|
||||
final Status.MediaAttachment.Type type = attachments[i].type;
|
||||
|
||||
previews[i].setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onViewMedia(url, type);
|
||||
}
|
||||
});
|
||||
if(url == null || url.isEmpty()) {
|
||||
previews[i].setOnClickListener(null);
|
||||
} else {
|
||||
previews[i].setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
listener.onViewMedia(url, type);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
if (sensitive) {
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
/* 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;
|
||||
|
||||
/**
|
||||
* This is just a wrapper class for a String.
|
||||
*
|
||||
* It was designed to get around the limitation of a Json deserializer which only allows custom
|
||||
* deserializing based on types, when special handling for a specific field was what was actually
|
||||
* desired (in this case, display names). So, it was most expedient to just make up a type.
|
||||
*/
|
||||
public class StringWithEmoji {
|
||||
public String value;
|
||||
|
||||
public StringWithEmoji(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/* 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 com.emojione.Emojione;
|
||||
import com.google.gson.JsonDeserializationContext;
|
||||
import com.google.gson.JsonDeserializer;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
/** This is a type-based workaround to allow for shortcode conversion when loading display names. */
|
||||
class StringWithEmojiTypeAdapter implements JsonDeserializer<StringWithEmoji> {
|
||||
@Override
|
||||
public StringWithEmoji deserialize(JsonElement json, Type typeOfT,
|
||||
JsonDeserializationContext context) throws JsonParseException {
|
||||
String value = json.getAsString();
|
||||
if (value != null) {
|
||||
return new StringWithEmoji(Emojione.shortnameToUnicode(value, false));
|
||||
} else {
|
||||
return new StringWithEmoji("");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -65,20 +65,55 @@ class ThreadAdapter extends RecyclerView.Adapter implements AdapterItemRemover {
|
|||
notifyItemRemoved(position);
|
||||
}
|
||||
|
||||
int insertStatus(Status status) {
|
||||
public void removeAllByAccountId(String accountId) {
|
||||
for (int i = 0; i < statuses.size();) {
|
||||
Status status = statuses.get(i);
|
||||
if (accountId.equals(status.account.id)) {
|
||||
statuses.remove(i);
|
||||
notifyItemRemoved(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int setStatus(Status status) {
|
||||
if (statuses.size() > 0 && statuses.get(statusIndex).equals(status)) {
|
||||
// Do not add this status on refresh, it's already in there.
|
||||
statuses.set(statusIndex, status);
|
||||
return statusIndex;
|
||||
}
|
||||
int i = statusIndex;
|
||||
statuses.add(i, status);
|
||||
notifyItemInserted(i);
|
||||
return i;
|
||||
}
|
||||
|
||||
void addAncestors(List<Status> ancestors) {
|
||||
void setContext(List<Status> ancestors, List<Status> descendants) {
|
||||
Status mainStatus = null;
|
||||
|
||||
// In case of refresh, remove old ancestors and descendants first. We'll remove all blindly,
|
||||
// as we have no guarantee on their order to be the same as before
|
||||
int old_size = statuses.size();
|
||||
if (old_size > 0) {
|
||||
mainStatus = statuses.get(statusIndex);
|
||||
statuses.clear();
|
||||
notifyItemRangeRemoved(0, old_size);
|
||||
}
|
||||
|
||||
// Insert newly fetched ancestors
|
||||
statusIndex = ancestors.size();
|
||||
statuses.addAll(0, ancestors);
|
||||
notifyItemRangeInserted(0, statusIndex);
|
||||
}
|
||||
|
||||
void addDescendants(List<Status> descendants) {
|
||||
if (mainStatus != null) {
|
||||
// In case we needed to delete everything (which is way easier than deleting
|
||||
// everything except one), re-insert the remaining status here.
|
||||
statuses.add(statusIndex, mainStatus);
|
||||
notifyItemInserted(statusIndex);
|
||||
}
|
||||
|
||||
// Insert newly fetched descendants
|
||||
int end = statuses.size();
|
||||
statuses.addAll(descendants);
|
||||
notifyItemRangeInserted(end, descendants.size());
|
||||
|
|
|
@ -30,8 +30,15 @@ class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover
|
|||
private static final int VIEW_TYPE_STATUS = 0;
|
||||
private static final int VIEW_TYPE_FOOTER = 1;
|
||||
|
||||
enum FooterState {
|
||||
EMPTY,
|
||||
END,
|
||||
LOADING
|
||||
}
|
||||
|
||||
private List<Status> statuses;
|
||||
private StatusActionListener statusListener;
|
||||
private FooterState footerState = FooterState.END;
|
||||
|
||||
TimelineAdapter(StatusActionListener statusListener) {
|
||||
super();
|
||||
|
@ -49,13 +56,37 @@ class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover
|
|||
return new StatusViewHolder(view);
|
||||
}
|
||||
case VIEW_TYPE_FOOTER: {
|
||||
View view = LayoutInflater.from(viewGroup.getContext())
|
||||
.inflate(R.layout.item_footer, viewGroup, false);
|
||||
View view;
|
||||
switch (footerState) {
|
||||
default:
|
||||
case LOADING:
|
||||
view = LayoutInflater.from(viewGroup.getContext())
|
||||
.inflate(R.layout.item_footer, viewGroup, false);
|
||||
break;
|
||||
case END: {
|
||||
view = LayoutInflater.from(viewGroup.getContext())
|
||||
.inflate(R.layout.item_footer_end, viewGroup, false);
|
||||
break;
|
||||
}
|
||||
case EMPTY: {
|
||||
view = LayoutInflater.from(viewGroup.getContext())
|
||||
.inflate(R.layout.item_footer_empty, viewGroup, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return new FooterViewHolder(view);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void setFooterState(FooterState newFooterState) {
|
||||
FooterState oldValue = footerState;
|
||||
footerState = newFooterState;
|
||||
if (footerState != oldValue) {
|
||||
notifyItemChanged(statuses.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
|
||||
if (position < statuses.size()) {
|
||||
|
@ -116,6 +147,18 @@ class TimelineAdapter extends RecyclerView.Adapter implements AdapterItemRemover
|
|||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void removeAllByAccountId(String accountId) {
|
||||
for (int i = 0; i < statuses.size();) {
|
||||
Status status = statuses.get(i);
|
||||
if (accountId.equals(status.account.id)) {
|
||||
statuses.remove(i);
|
||||
notifyItemRemoved(i);
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
Status getItem(int position) {
|
||||
if (position >= 0 && position < statuses.size()) {
|
||||
|
|
|
@ -40,7 +40,10 @@ import retrofit2.Call;
|
|||
import retrofit2.Callback;
|
||||
|
||||
public class TimelineFragment extends SFragment implements
|
||||
SwipeRefreshLayout.OnRefreshListener, StatusActionListener {
|
||||
SwipeRefreshLayout.OnRefreshListener,
|
||||
StatusActionListener,
|
||||
StatusRemoveListener,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private static final String TAG = "Timeline"; // logging tag
|
||||
|
||||
private Call<List<Status>> listCall;
|
||||
|
@ -65,6 +68,7 @@ public class TimelineFragment extends SFragment implements
|
|||
private SharedPreferences preferences;
|
||||
private boolean filterRemoveReplies;
|
||||
private boolean filterRemoveReblogs;
|
||||
private boolean hideFab;
|
||||
|
||||
public static TimelineFragment newInstance(Kind kind) {
|
||||
TimelineFragment fragment = new TimelineFragment();
|
||||
|
@ -149,24 +153,28 @@ public class TimelineFragment extends SFragment implements
|
|||
|
||||
/* This is delayed until onActivityCreated solely because MainActivity.composeButton isn't
|
||||
* guaranteed to be set until then. */
|
||||
if (followButtonPresent()) {
|
||||
if (composeButtonPresent()) {
|
||||
/* Use a modified scroll listener that both loads more statuses as it goes, and hides
|
||||
* the follow button on down-scroll. */
|
||||
MainActivity activity = (MainActivity) getActivity();
|
||||
final FloatingActionButton composeButton = activity.composeButton;
|
||||
final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(
|
||||
activity);
|
||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
||||
hideFab = preferences.getBoolean("fabHide", false);
|
||||
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView view, int dx, int dy) {
|
||||
super.onScrolled(view, dx, dy);
|
||||
|
||||
if (preferences.getBoolean("fabHide", false)) {
|
||||
if (hideFab) {
|
||||
if (dy > 0 && composeButton.isShown()) {
|
||||
composeButton.hide(); // hides the button if we're scrolling down
|
||||
} else if (dy < 0 && !composeButton.isShown()) {
|
||||
composeButton.show(); // shows it if we are scrolling up
|
||||
}
|
||||
} else if (!composeButton.isShown()) {
|
||||
composeButton.show();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,7 +216,7 @@ public class TimelineFragment extends SFragment implements
|
|||
return kind != Kind.TAG && kind != Kind.FAVOURITES;
|
||||
}
|
||||
|
||||
private boolean followButtonPresent() {
|
||||
private boolean composeButtonPresent() {
|
||||
return kind != Kind.TAG && kind != Kind.FAVOURITES && kind != Kind.USER;
|
||||
}
|
||||
|
||||
|
@ -218,7 +226,9 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
|
||||
private void sendFetchTimelineRequest(@Nullable final String fromId, @Nullable String uptoId) {
|
||||
MastodonAPI api = ((BaseActivity) getActivity()).mastodonAPI;
|
||||
if (fromId != null || adapter.getItemCount() <= 1) {
|
||||
adapter.setFooterState(TimelineAdapter.FooterState.LOADING);
|
||||
}
|
||||
|
||||
Callback<List<Status>> cb = new Callback<List<Status>>() {
|
||||
@Override
|
||||
|
@ -239,27 +249,27 @@ public class TimelineFragment extends SFragment implements
|
|||
switch (kind) {
|
||||
default:
|
||||
case HOME: {
|
||||
listCall = api.homeTimeline(fromId, uptoId, null);
|
||||
listCall = mastodonAPI.homeTimeline(fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case PUBLIC_FEDERATED: {
|
||||
listCall = api.publicTimeline(null, fromId, uptoId, null);
|
||||
listCall = mastodonAPI.publicTimeline(null, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case PUBLIC_LOCAL: {
|
||||
listCall = api.publicTimeline(true, fromId, uptoId, null);
|
||||
listCall = mastodonAPI.publicTimeline(true, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case TAG: {
|
||||
listCall = api.hashtagTimeline(hashtagOrId, null, fromId, uptoId, null);
|
||||
listCall = mastodonAPI.hashtagTimeline(hashtagOrId, null, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case USER: {
|
||||
listCall = api.accountStatuses(hashtagOrId, fromId, uptoId, null);
|
||||
listCall = mastodonAPI.accountStatuses(hashtagOrId, fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
case FAVOURITES: {
|
||||
listCall = api.favourites(fromId, uptoId, null);
|
||||
listCall = mastodonAPI.favourites(fromId, uptoId, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -271,6 +281,10 @@ public class TimelineFragment extends SFragment implements
|
|||
sendFetchTimelineRequest(null, null);
|
||||
}
|
||||
|
||||
public void removePostsByUser(String accountId) {
|
||||
adapter.removeAllByAccountId(accountId);
|
||||
}
|
||||
|
||||
private static boolean findStatus(List<Status> statuses, String id) {
|
||||
for (Status status : statuses) {
|
||||
if (status.id.equals(id)) {
|
||||
|
@ -317,6 +331,11 @@ public class TimelineFragment extends SFragment implements
|
|||
} else {
|
||||
adapter.update(statuses);
|
||||
}
|
||||
if (statuses.size() == 0 && adapter.getItemCount() == 1) {
|
||||
adapter.setFooterState(TimelineAdapter.FooterState.EMPTY);
|
||||
} else if(fromId != null) {
|
||||
adapter.setFooterState(TimelineAdapter.FooterState.END);
|
||||
}
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
}
|
||||
|
||||
|
@ -334,6 +353,14 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccessfulStatus() {
|
||||
if (kind == Kind.HOME || kind == Kind.PUBLIC_FEDERATED || kind == Kind.PUBLIC_LOCAL) {
|
||||
onRefresh();
|
||||
}
|
||||
super.onSuccessfulStatus();
|
||||
}
|
||||
|
||||
public void onReply(int position) {
|
||||
super.reply(adapter.getItem(position));
|
||||
}
|
||||
|
@ -374,4 +401,11 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
super.viewAccount(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if(key.equals("fabHide")) {
|
||||
hideFab = sharedPreferences.getBoolean("fabHide", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,10 +18,35 @@ package com.keylesspalace.tusky;
|
|||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class TimelinePagerAdapter extends FragmentPagerAdapter {
|
||||
private int currentFragmentIndex;
|
||||
private List<Fragment> registeredFragments;
|
||||
|
||||
TimelinePagerAdapter(FragmentManager manager) {
|
||||
super(manager);
|
||||
currentFragmentIndex = 0;
|
||||
registeredFragments = new ArrayList<>();
|
||||
}
|
||||
|
||||
Fragment getCurrentFragment() {
|
||||
return registeredFragments.get(currentFragmentIndex);
|
||||
}
|
||||
|
||||
List<Fragment> getRegisteredFragments() {
|
||||
return registeredFragments;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPrimaryItem(ViewGroup container, int position, Object object) {
|
||||
if (position != currentFragmentIndex) {
|
||||
currentFragmentIndex = position;
|
||||
}
|
||||
super.setPrimaryItem(container, position, object);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -54,4 +79,17 @@ class TimelinePagerAdapter extends FragmentPagerAdapter {
|
|||
public CharSequence getPageTitle(int position) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object instantiateItem(ViewGroup container, int position) {
|
||||
Fragment fragment = (Fragment) super.instantiateItem(container, position);
|
||||
registeredFragments.add(fragment);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(ViewGroup container, int position, Object object) {
|
||||
registeredFragments.remove((Fragment) object);
|
||||
super.destroyItem(container, position, object);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
/* 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.app.Application;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.squareup.picasso.Picasso;
|
||||
import com.jakewharton.picasso.OkHttp3Downloader;
|
||||
|
||||
public class TuskyApplication extends Application {
|
||||
@Override
|
||||
public void onCreate() {
|
||||
// Initialize Picasso configuration
|
||||
Picasso.Builder builder = new Picasso.Builder(this);
|
||||
builder.downloader(new OkHttp3Downloader(this));
|
||||
if (BuildConfig.DEBUG) {
|
||||
builder.listener(new Picasso.Listener() {
|
||||
@Override
|
||||
public void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
Picasso.setSingletonInstance(builder.build());
|
||||
} catch (IllegalStateException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Picasso.with(this).setLoggingEnabled(true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,9 +15,21 @@
|
|||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.StringRes;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
@ -27,6 +39,8 @@ import android.view.WindowManager;
|
|||
import com.squareup.picasso.Callback;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import uk.co.senab.photoview.PhotoView;
|
||||
|
@ -35,6 +49,9 @@ import uk.co.senab.photoview.PhotoViewAttacher;
|
|||
public class ViewMediaFragment extends DialogFragment {
|
||||
|
||||
private PhotoViewAttacher attacher;
|
||||
private DownloadManager downloadManager;
|
||||
|
||||
private static final int PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE = 1;
|
||||
|
||||
@BindView(R.id.view_media_image) PhotoView photoView;
|
||||
|
||||
|
@ -99,6 +116,25 @@ public class ViewMediaFragment extends DialogFragment {
|
|||
}
|
||||
});
|
||||
|
||||
attacher.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
|
||||
AlertDialog downloadDialog = new AlertDialog.Builder(getContext()).create();
|
||||
|
||||
downloadDialog.setButton(AlertDialog.BUTTON_NEUTRAL, getString(R.string.dialog_download_image),
|
||||
new DialogInterface.OnClickListener() {
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
dialog.dismiss();
|
||||
|
||||
downloadImage();
|
||||
}
|
||||
});
|
||||
downloadDialog.show();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
Picasso.with(getContext())
|
||||
.load(url)
|
||||
.into(photoView, new Callback() {
|
||||
|
@ -121,4 +157,63 @@ public class ViewMediaFragment extends DialogFragment {
|
|||
attacher.cleanup();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void downloadImage(){
|
||||
|
||||
//Permission stuff
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
|
||||
ContextCompat.checkSelfPermission(this.getContext(), android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
android.support.v4.app.ActivityCompat.requestPermissions(getActivity(),
|
||||
new String[] { android.Manifest.permission.WRITE_EXTERNAL_STORAGE },
|
||||
PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE);
|
||||
} else {
|
||||
|
||||
|
||||
//download stuff
|
||||
String url = getArguments().getString("url");
|
||||
Uri uri = Uri.parse(url);
|
||||
|
||||
String filename = new File(url).getName();
|
||||
|
||||
downloadManager = (DownloadManager) getContext().getSystemService(Context.DOWNLOAD_SERVICE);
|
||||
|
||||
DownloadManager.Request request = new DownloadManager.Request(uri);
|
||||
request.allowScanningByMediaScanner();
|
||||
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_PICTURES, getString(R.string.app_name) + "/" + filename);
|
||||
|
||||
downloadManager.enqueue(request);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[],
|
||||
@NonNull int[] grantResults) {
|
||||
switch (requestCode) {
|
||||
case PERMISSIONS_REQUEST_WRITE_EXTERNAL_STORAGE: {
|
||||
if (grantResults.length > 0
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
downloadImage();
|
||||
} else {
|
||||
doErrorDialog(R.string.error_media_download_permission, R.string.action_retry,
|
||||
new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
downloadImage();
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void doErrorDialog(@StringRes int descriptionId, @StringRes int actionId,
|
||||
View.OnClickListener listener) {
|
||||
Snackbar bar = Snackbar.make(getView(), getString(descriptionId),
|
||||
Snackbar.LENGTH_SHORT);
|
||||
bar.setAction(actionId, listener);
|
||||
bar.show();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,7 +26,9 @@ import android.view.MenuItem;
|
|||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
|
||||
public class ViewTagActivity extends BaseActivity {
|
||||
public class ViewTagActivity extends BaseActivity implements SFragment.OnUserRemovedListener {
|
||||
private Fragment timelineFragment;
|
||||
|
||||
@BindView(R.id.toolbar) Toolbar toolbar;
|
||||
|
||||
@Override
|
||||
|
@ -51,6 +53,8 @@ public class ViewTagActivity extends BaseActivity {
|
|||
Fragment fragment = TimelineFragment.newInstance(TimelineFragment.Kind.TAG, hashtag);
|
||||
fragmentTransaction.add(R.id.fragment_container, fragment);
|
||||
fragmentTransaction.commit();
|
||||
|
||||
timelineFragment = fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -63,4 +67,10 @@ public class ViewTagActivity extends BaseActivity {
|
|||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserRemoved(String accountId) {
|
||||
StatusRemoveListener listener = (StatusRemoveListener) timelineFragment;
|
||||
listener.removePostsByUser(accountId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,7 +24,9 @@ import android.support.v7.widget.Toolbar;
|
|||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
|
||||
public class ViewThreadActivity extends BaseActivity {
|
||||
public class ViewThreadActivity extends BaseActivity implements SFragment.OnUserRemovedListener {
|
||||
Fragment viewThreadFragment;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
@ -44,6 +46,8 @@ public class ViewThreadActivity extends BaseActivity {
|
|||
Fragment fragment = ViewThreadFragment.newInstance(id);
|
||||
fragmentTransaction.add(R.id.fragment_container, fragment);
|
||||
fragmentTransaction.commit();
|
||||
|
||||
viewThreadFragment = fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -62,4 +66,12 @@ public class ViewThreadActivity extends BaseActivity {
|
|||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUserRemoved(String accountId) {
|
||||
if (viewThreadFragment instanceof StatusRemoveListener) {
|
||||
StatusRemoveListener listener = (StatusRemoveListener) viewThreadFragment;
|
||||
listener.removePostsByUser(accountId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.os.Bundle;
|
|||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.Snackbar;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.widget.DividerItemDecoration;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
|
@ -34,9 +35,11 @@ import com.keylesspalace.tusky.entity.StatusContext;
|
|||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
|
||||
public class ViewThreadFragment extends SFragment implements StatusActionListener {
|
||||
public class ViewThreadFragment extends SFragment implements
|
||||
SwipeRefreshLayout.OnRefreshListener, StatusActionListener, StatusRemoveListener {
|
||||
private static final String TAG = "ViewThreadFragment";
|
||||
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private RecyclerView recyclerView;
|
||||
private ThreadAdapter adapter;
|
||||
private String thisThreadsStatusId;
|
||||
|
@ -56,6 +59,9 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
|
|||
View rootView = inflater.inflate(R.layout.fragment_view_thread, container, false);
|
||||
|
||||
Context context = getContext();
|
||||
swipeRefreshLayout = (SwipeRefreshLayout) rootView.findViewById(R.id.swipe_refresh_layout);
|
||||
swipeRefreshLayout.setOnRefreshListener(this);
|
||||
|
||||
recyclerView = (RecyclerView) rootView.findViewById(R.id.recycler_view);
|
||||
recyclerView.setHasFixedSize(true);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(context);
|
||||
|
@ -86,7 +92,7 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
|
|||
@Override
|
||||
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
|
||||
if (response.isSuccessful()) {
|
||||
int position = adapter.insertStatus(response.body());
|
||||
int position = adapter.setStatus(response.body());
|
||||
recyclerView.scrollToPosition(position);
|
||||
} else {
|
||||
onThreadRequestFailure(id);
|
||||
|
@ -109,10 +115,10 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
|
|||
@Override
|
||||
public void onResponse(Call<StatusContext> call, retrofit2.Response<StatusContext> response) {
|
||||
if (response.isSuccessful()) {
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
StatusContext context = response.body();
|
||||
|
||||
adapter.addAncestors(context.ancestors);
|
||||
adapter.addDescendants(context.descendants);
|
||||
adapter.setContext(context.ancestors, context.descendants);
|
||||
} else {
|
||||
onThreadRequestFailure(id);
|
||||
}
|
||||
|
@ -128,6 +134,7 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
|
|||
|
||||
private void onThreadRequestFailure(final String id) {
|
||||
View view = getView();
|
||||
swipeRefreshLayout.setRefreshing(false);
|
||||
if (view != null) {
|
||||
Snackbar.make(view, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||
.setAction(R.string.action_retry, new View.OnClickListener() {
|
||||
|
@ -143,6 +150,22 @@ public class ViewThreadFragment extends SFragment implements StatusActionListene
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removePostsByUser(String accountId) {
|
||||
adapter.removeAllByAccountId(accountId);
|
||||
}
|
||||
|
||||
public void onRefresh() {
|
||||
sendStatusRequest(thisThreadsStatusId);
|
||||
sendThreadRequest(thisThreadsStatusId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccessfulStatus() {
|
||||
onRefresh();
|
||||
super.onSuccessfulStatus();
|
||||
}
|
||||
|
||||
public void onReply(int position) {
|
||||
super.reply(adapter.getItem(position));
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import android.text.Spanned;
|
|||
import com.arlib.floatingsearchview.suggestions.model.SearchSuggestion;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.keylesspalace.tusky.HtmlUtils;
|
||||
import com.keylesspalace.tusky.StringWithEmoji;
|
||||
|
||||
public class Account implements SearchSuggestion {
|
||||
public String id;
|
||||
|
@ -32,7 +33,7 @@ public class Account implements SearchSuggestion {
|
|||
public String username;
|
||||
|
||||
@SerializedName("display_name")
|
||||
public String displayName;
|
||||
public StringWithEmoji displayName;
|
||||
|
||||
public Spanned note;
|
||||
|
||||
|
@ -70,11 +71,10 @@ public class Account implements SearchSuggestion {
|
|||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
if (displayName.length() == 0) {
|
||||
if (displayName.value.length() == 0) {
|
||||
return localUsername;
|
||||
}
|
||||
|
||||
return displayName;
|
||||
return displayName.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -92,7 +92,7 @@ public class Account implements SearchSuggestion {
|
|||
dest.writeString(id);
|
||||
dest.writeString(localUsername);
|
||||
dest.writeString(username);
|
||||
dest.writeString(displayName);
|
||||
dest.writeString(displayName.value);
|
||||
dest.writeString(HtmlUtils.toHtml(note));
|
||||
dest.writeString(url);
|
||||
dest.writeString(avatar);
|
||||
|
@ -111,7 +111,7 @@ public class Account implements SearchSuggestion {
|
|||
id = in.readString();
|
||||
localUsername = in.readString();
|
||||
username = in.readString();
|
||||
displayName = in.readString();
|
||||
displayName = new StringWithEmoji(in.readString());
|
||||
note = HtmlUtils.fromHtml(in.readString());
|
||||
url = in.readString();
|
||||
avatar = in.readString();
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
package com.keylesspalace.tusky.entity;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class Profile {
|
||||
@SerializedName("display_name")
|
||||
public String displayName;
|
||||
|
||||
@SerializedName("note")
|
||||
public String note;
|
||||
|
||||
/** Encoded in Base-64 */
|
||||
@SerializedName("avatar")
|
||||
public String avatar;
|
||||
|
||||
/** Encoded in Base-64 */
|
||||
@SerializedName("header")
|
||||
public String header;
|
||||
}
|
|
@ -146,5 +146,8 @@ public class Status {
|
|||
|
||||
@SerializedName("acct")
|
||||
public String username;
|
||||
|
||||
@SerializedName("username")
|
||||
public String localUsername;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue