diff --git a/app/src/fdroid/java/com/keylesspalace/tusky/MessagingService.java b/app/src/fdroid/java/com/keylesspalace/tusky/MessagingService.java index f7c24c42..01d98122 100644 --- a/app/src/fdroid/java/com/keylesspalace/tusky/MessagingService.java +++ b/app/src/fdroid/java/com/keylesspalace/tusky/MessagingService.java @@ -21,14 +21,18 @@ import android.content.Intent; import android.content.SharedPreferences; import android.preference.PreferenceManager; import android.text.Spanned; +import android.util.ArraySet; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.keylesspalace.tusky.entity.Notification; + +import java.util.HashSet; import java.util.List; import java.io.IOException; +import java.util.Set; import okhttp3.Interceptor; import okhttp3.OkHttpClient; @@ -64,10 +68,7 @@ public class MessagingService extends IntentService { public void onResponse(Call> call, Response> response) { if (response.isSuccessful()) { - List notificationList = response.body(); - for (Notification notification : notificationList) { - NotificationMaker.make(MessagingService.this, NOTIFY_ID, notification); - } + onNotificationsReceived(response.body()); } } @@ -111,4 +112,21 @@ public class MessagingService extends IntentService { mastodonAPI = retrofit.create(MastodonAPI.class); } + + private void onNotificationsReceived(List notificationList) { + SharedPreferences notificationsPreferences = getSharedPreferences( + "Notifications", Context.MODE_PRIVATE); + Set currentIds = notificationsPreferences.getStringSet( + "current_ids", new HashSet()); + for (Notification notification : notificationList) { + String id = notification.id; + if (!currentIds.contains(id)) { + currentIds.add(id); + NotificationMaker.make(this, NOTIFY_ID, notification); + } + } + notificationsPreferences.edit() + .putStringSet("current_ids", currentIds) + .apply(); + } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fcf90d73..84c1b093 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -64,7 +64,7 @@ - + diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountActionListener.java b/app/src/main/java/com/keylesspalace/tusky/AccountActionListener.java index 5c3e4950..bca609cd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountActionListener.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountActionListener.java @@ -19,4 +19,5 @@ 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); } diff --git a/app/src/main/java/com/keylesspalace/tusky/BlocksActivity.java b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java similarity index 76% rename from app/src/main/java/com/keylesspalace/tusky/BlocksActivity.java rename to app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java index 57a92f46..1d1276fb 100644 --- a/app/src/main/java/com/keylesspalace/tusky/BlocksActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountListActivity.java @@ -24,16 +24,17 @@ 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 + 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(); @@ -48,21 +49,29 @@ public class BlocksActivity extends BaseActivity { ActionBar bar = getSupportActionBar(); if (bar != null) { switch (type) { - case MUTES: { bar.setTitle(getString(R.string.title_mutes)); break; } 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(); - AccountFragment.Type fragmentType; + AccountListFragment.Type fragmentType; switch (type) { - case MUTES: { fragmentType = AccountFragment.Type.MUTES; break; } default: - case BLOCKS: { fragmentType = AccountFragment.Type.BLOCKS; break; } + 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 = AccountFragment.newInstance(fragmentType); + Fragment fragment = AccountListFragment.newInstance(fragmentType); fragmentTransaction.add(R.id.fragment_container, fragment); fragmentTransaction.commit(); } diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java b/app/src/main/java/com/keylesspalace/tusky/AccountListFragment.java similarity index 82% rename from app/src/main/java/com/keylesspalace/tusky/AccountFragment.java rename to app/src/main/java/com/keylesspalace/tusky/AccountListFragment.java index c4c354ad..1128602f 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountListFragment.java @@ -38,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> 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; @@ -59,17 +58,17 @@ 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(); + 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(); + AccountListFragment fragment = new AccountListFragment(); arguments.putSerializable("type", type); arguments.putString("accountId", accountId); fragment.setArguments(arguments); @@ -90,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); @@ -108,6 +107,8 @@ public class AccountFragment extends BaseFragment implements AccountActionListen 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); } @@ -157,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()) { @@ -189,6 +184,7 @@ public class AccountFragment extends BaseFragment implements AccountActionListen } }; + Call> listCall; switch (type) { default: case FOLLOWS: { @@ -207,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); @@ -239,12 +239,14 @@ 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 @@ -308,6 +310,7 @@ public class AccountFragment extends BaseFragment implements AccountActionListen 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 @@ -371,6 +374,54 @@ 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 callback = new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + onRespondToFollowRequestSuccess(position); + } else { + onRespondToFollowRequestFailure(accept, accountId); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + onRespondToFollowRequestFailure(accept, accountId); + } + }; + + Call 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.FOLLOWS || type == Type.FOLLOWERS; } diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountPagerAdapter.java b/app/src/main/java/com/keylesspalace/tusky/AccountPagerAdapter.java index dd56dfad..505d89bd 100644 --- a/app/src/main/java/com/keylesspalace/tusky/AccountPagerAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/AccountPagerAdapter.java @@ -51,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; diff --git a/app/src/main/java/com/keylesspalace/tusky/FollowRequestsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/FollowRequestsAdapter.java new file mode 100644 index 00000000..0a54b558 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/FollowRequestsAdapter.java @@ -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 . */ + +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); + } + }); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java index d25729a2..46963e24 100644 --- a/app/src/main/java/com/keylesspalace/tusky/MainActivity.java +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.java @@ -304,18 +304,22 @@ public class MainActivity extends BaseActivity implements SFragment.OnUserRemove Intent intent = new Intent(MainActivity.this, FavouritesActivity.class); startActivity(intent); } else if (drawerItemIdentifier == 2) { - Intent intent = new Intent(MainActivity.this, BlocksActivity.class); - intent.putExtra("type", BlocksActivity.Type.MUTES); + 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, BlocksActivity.class); - intent.putExtra("type", BlocksActivity.Type.BLOCKS); + 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); } } @@ -443,36 +447,7 @@ public class MainActivity extends BaseActivity implements SFragment.OnUserRemove onFetchUserInfoFailure(new Exception(response.message())); return; } - - headerResult.clear(); - - 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 @@ -482,9 +457,48 @@ public class MainActivity extends BaseActivity implements SFragment.OnUserRemove }); } - private void onFetchUserInfoSuccess(String id, String username) { - loggedInAccountId = id; - loggedInAccountUsername = username; + 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) diff --git a/app/src/main/res/drawable/ic_check_24dp.xml b/app/src/main/res/drawable/ic_check_24dp.xml index f73e39df..6541ee3e 100644 --- a/app/src/main/res/drawable/ic_check_24dp.xml +++ b/app/src/main/res/drawable/ic_check_24dp.xml @@ -1,7 +1,9 @@ - - - + + diff --git a/app/src/main/res/drawable/ic_check_in_box_24dp.xml b/app/src/main/res/drawable/ic_check_in_box_24dp.xml new file mode 100644 index 00000000..f73e39df --- /dev/null +++ b/app/src/main/res/drawable/ic_check_in_box_24dp.xml @@ -0,0 +1,7 @@ + + + + diff --git a/app/src/main/res/drawable/ic_reject_24dp.xml b/app/src/main/res/drawable/ic_reject_24dp.xml new file mode 100644 index 00000000..d11cc5c9 --- /dev/null +++ b/app/src/main/res/drawable/ic_reject_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_blocks.xml b/app/src/main/res/layout/activity_account_list.xml similarity index 90% rename from app/src/main/res/layout/activity_blocks.xml rename to app/src/main/res/layout/activity_account_list.xml index a305a19f..c5d72f85 100644 --- a/app/src/main/res/layout/activity_blocks.xml +++ b/app/src/main/res/layout/activity_account_list.xml @@ -5,7 +5,7 @@ android:id="@+id/activity_view_thread" android:layout_width="match_parent" android:layout_height="match_parent" - tools:context="com.keylesspalace.tusky.BlocksActivity"> + tools:context="com.keylesspalace.tusky.AccountListActivity"> + android:layout_centerVertical="true" + android:contentDescription="@string/action_view_profile" /> + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_muted_user.xml b/app/src/main/res/layout/item_muted_user.xml index bf1d89d9..34cce394 100644 --- a/app/src/main/res/layout/item_muted_user.xml +++ b/app/src/main/res/layout/item_muted_user.xml @@ -13,7 +13,8 @@ android:id="@+id/muted_user_avatar" android:layout_alignParentLeft="true" android:layout_marginRight="24dp" - android:layout_centerVertical="true"/> + android:layout_centerVertical="true" + android:contentDescription="@string/action_view_profile" /> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 966853bf..95e9bae5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,6 +31,7 @@ Favourites Muted users Blocked users + Follow Requests \@%s %s boosted @@ -78,6 +79,7 @@ Favourites Muted users Blocked users + Follow Requests Thread Media Open in browser @@ -95,6 +97,8 @@ Save Edit profile Undo + Accept + Reject Share toot URL to… Share toot to…