Follow requests list is available. Closes #222

This commit is contained in:
Vavassor 2017-04-28 23:52:14 -04:00
parent 41088de6be
commit 7d83a9aaba
18 changed files with 403 additions and 77 deletions

View file

@ -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<List<Notification>> call,
Response<List<Notification>> response) {
if (response.isSuccessful()) {
List<Notification> 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<Notification> notificationList) {
SharedPreferences notificationsPreferences = getSharedPreferences(
"Notifications", Context.MODE_PRIVATE);
Set<String> currentIds = notificationsPreferences.getStringSet(
"current_ids", new HashSet<String>());
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();
}
}

View file

@ -64,7 +64,7 @@
<activity android:name=".EditProfileActivity" />
<activity android:name=".PreferencesActivity" />
<activity android:name=".FavouritesActivity" />
<activity android:name=".BlocksActivity" />
<activity android:name=".AccountListActivity" />
<activity
android:name=".ReportActivity"
android:windowSoftInputMode="stateVisible|adjustResize" />

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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<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;
@ -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<List<Account>> 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<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.FOLLOWS || type == Type.FOLLOWERS;
}

View file

@ -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;

View file

@ -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);
}
});
}
}
}

View file

@ -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)

View file

@ -1,7 +1,9 @@
<vector android:height="24dp" android:viewportHeight="35.43307"
android:viewportWidth="35.43307" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M32.16,4.46L31.62,5.01L14.63,21.99L5.78,13.13L2.5,16.41L14.52,28.43L14.55,28.41L14.66,28.52L35.44,7.74L34.89,7.19C34.17,6.46 33.44,5.74 32.71,5.01L32.16,4.46z" />
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m1.1,6.19c-0.58,0 -1.07,0.49 -1.07,1.07l0,23.06c0,0.58 0.49,1.07 1.07,1.07l23.06,0c0.58,0 1.07,-0.49 1.07,-1.07l0,-18.89 -1.54,1.54 0,16.88 -22.12,0 0,-22.12 22.12,0 0,2.83 1.54,-1.54 0,-1.76c0,-0.58 -0.49,-1.07 -1.07,-1.07l-23.06,0z" />
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View file

@ -0,0 +1,7 @@
<vector android:height="24dp" android:viewportHeight="35.43307"
android:viewportWidth="35.43307" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M32.16,4.46L31.62,5.01L14.63,21.99L5.78,13.13L2.5,16.41L14.52,28.43L14.55,28.41L14.66,28.52L35.44,7.74L34.89,7.19C34.17,6.46 33.44,5.74 32.71,5.01L32.16,4.46z" />
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m1.1,6.19c-0.58,0 -1.07,0.49 -1.07,1.07l0,23.06c0,0.58 0.49,1.07 1.07,1.07l23.06,0c0.58,0 1.07,-0.49 1.07,-1.07l0,-18.89 -1.54,1.54 0,16.88 -22.12,0 0,-22.12 22.12,0 0,2.83 1.54,-1.54 0,-1.76c0,-0.58 -0.49,-1.07 -1.07,-1.07l-23.06,0z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View file

@ -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">
<LinearLayout
android:layout_width="match_parent"

View file

@ -13,7 +13,8 @@
android:id="@+id/blocked_user_avatar"
android:layout_alignParentLeft="true"
android:layout_marginRight="24dp"
android:layout_centerVertical="true"/>
android:layout_centerVertical="true"
android:contentDescription="@string/action_view_profile" />
<ImageButton
app:srcCompat="@drawable/ic_clear_24dp"

View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="72dp"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:gravity="center_vertical">
<com.pkmmte.view.CircularImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:id="@+id/follow_request_avatar"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_marginRight="24dp"
android:layout_marginEnd="24dp"
android:layout_centerVertical="true"
android:contentDescription="@string/action_view_profile" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical"
android:layout_toRightOf="@id/follow_request_avatar"
android:layout_toEndOf="@id/follow_request_avatar"
android:layout_toLeftOf="@+id/follow_request_accept"
android:layout_toStartOf="@id/follow_request_accept">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/follow_request_display_name"
android:text="Display name"
android:maxLines="1"
android:ellipsize="end"
android:textSize="16sp"
android:textColor="?android:textColorPrimary"
android:textStyle="normal|bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="\@username"
android:maxLines="1"
android:ellipsize="end"
android:textSize="14sp"
android:id="@+id/follow_request_username"
android:textColor="?android:textColorSecondary" />
</LinearLayout>
<ImageButton
android:layout_width="24dp"
android:layout_height="24dp"
style="?attr/image_button_style"
android:id="@+id/follow_request_accept"
app:srcCompat="@drawable/ic_check_24dp"
android:layout_toLeftOf="@+id/follow_request_reject"
android:layout_toStartOf="@id/follow_request_reject"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_centerVertical="true"
android:contentDescription="@string/action_accept" />
<ImageButton
android:layout_width="24dp"
android:layout_height="24dp"
style="?attr/image_button_style"
android:id="@id/follow_request_reject"
app:srcCompat="@drawable/ic_reject_24dp"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
android:layout_centerVertical="true"
android:contentDescription="@string/action_reject" />
</RelativeLayout>

View file

@ -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" />
<ImageButton
app:srcCompat="@drawable/ic_unmute_24dp"

View file

@ -3,7 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_save"
android:icon="@drawable/ic_check_24dp"
android:icon="@drawable/ic_check_in_box_24dp"
android:title="@string/action_save"
app:showAsAction="always" />
</menu>

View file

@ -31,6 +31,7 @@
<string name="title_favourites">Favourites</string>
<string name="title_mutes">Muted users</string>
<string name="title_blocks">Blocked users</string>
<string name="title_follow_requests">Follow Requests</string>
<string name="status_username_format">\@%s</string>
<string name="status_boosted_format">%s boosted</string>
@ -78,6 +79,7 @@
<string name="action_view_favourites">Favourites</string>
<string name="action_view_mutes">Muted users</string>
<string name="action_view_blocks">Blocked users</string>
<string name="action_view_follow_requests">Follow Requests</string>
<string name="action_view_thread">Thread</string>
<string name="action_view_media">Media</string>
<string name="action_open_in_web">Open in browser</string>
@ -95,6 +97,8 @@
<string name="action_save">Save</string>
<string name="action_edit_profile">Edit profile</string>
<string name="action_undo">Undo</string>
<string name="action_accept">Accept</string>
<string name="action_reject">Reject</string>
<string name="send_status_link_to">Share toot URL to…</string>
<string name="send_status_content_to">Share toot to…</string>