Skip to content

Commit

Permalink
Add notifications for follow requests
Browse files Browse the repository at this point in the history
  • Loading branch information
Tak committed Mar 17, 2020
1 parent 6a87e3e commit 736d1ca
Show file tree
Hide file tree
Showing 15 changed files with 995 additions and 163 deletions.
735 changes: 735 additions & 0 deletions app/schemas/com.keylesspalace.tusky.db.AppDatabase/22.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.keylesspalace.tusky.adapter

import android.view.View
import androidx.core.text.BidiFormatter
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView
import com.keylesspalace.tusky.R
import com.keylesspalace.tusky.entity.Account
import com.keylesspalace.tusky.interfaces.AccountActionListener
import com.keylesspalace.tusky.util.CustomEmojiHelper
import com.keylesspalace.tusky.util.loadAvatar
import com.keylesspalace.tusky.util.visible
import kotlinx.android.synthetic.main.item_follow_request.view.*

internal class FollowRequestViewHolder(itemView: View, private val showHeader: Boolean) : RecyclerView.ViewHolder(itemView) {
private var id: String? = null
private val animateAvatar: Boolean = PreferenceManager.getDefaultSharedPreferences(itemView.context)
.getBoolean("animateGifAvatars", false)

fun setupWithAccount(account: Account, formatter: BidiFormatter?) {
id = account.id
val wrappedName = formatter?.unicodeWrap(account.name) ?: account.name
val emojifiedName: CharSequence = CustomEmojiHelper.emojifyString(wrappedName, account.emojis, itemView)
itemView.displayNameTextView.text = emojifiedName
if (showHeader) {
itemView.notificationTextView.text = itemView.context.getString(R.string.notification_follow_request_format, emojifiedName)
}
itemView.notificationTextView.visible(showHeader)
val format = itemView.context.getString(R.string.status_username_format)
val formattedUsername = String.format(format, account.username)
itemView.usernameTextView.text = formattedUsername
val avatarRadius = itemView.avatar.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp)
loadAvatar(account.avatar, itemView.avatar, avatarRadius, animateAvatar)
}

fun setupActionListener(listener: AccountActionListener) {
itemView.acceptButton.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
listener.onRespondToFollowRequest(true, id, position)
}
}
itemView.rejectButton.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
listener.onRespondToFollowRequest(false, id, position)
}
}
itemView.avatar.setOnClickListener { listener.onViewAccount(id) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,12 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
import androidx.recyclerview.widget.RecyclerView;

import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.util.CustomEmojiHelper;
import com.keylesspalace.tusky.util.ImageLoadingHelper;

public class FollowRequestsAdapter extends AccountAdapter {

Expand All @@ -46,7 +39,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int
case VIEW_TYPE_ACCOUNT: {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_follow_request, parent, false);
return new FollowRequestViewHolder(view);
return new FollowRequestViewHolder(view, false);
}
case VIEW_TYPE_FOOTER: {
View view = LayoutInflater.from(parent.getContext())
Expand All @@ -60,57 +53,8 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(accountList.get(position));
holder.setupWithAccount(accountList.get(position), null);
holder.setupActionListener(accountActionListener);
}
}

static class FollowRequestViewHolder extends RecyclerView.ViewHolder {
private ImageView avatar;
private TextView username;
private TextView displayName;
private ImageButton accept;
private ImageButton reject;
private String id;
private boolean animateAvatar;

FollowRequestViewHolder(View itemView) {
super(itemView);
avatar = itemView.findViewById(R.id.avatar);
username = itemView.findViewById(R.id.usernameTextView);
displayName = itemView.findViewById(R.id.displayNameTextView);
accept = itemView.findViewById(R.id.acceptButton);
reject = itemView.findViewById(R.id.rejectButton);
animateAvatar = PreferenceManager.getDefaultSharedPreferences(itemView.getContext())
.getBoolean("animateGifAvatars", false);
}

void setupWithAccount(Account account) {
id = account.getId();
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(account.getName(), account.getEmojis(), displayName);
displayName.setText(emojifiedName);
String format = username.getContext().getString(R.string.status_username_format);
String formattedUsername = String.format(format, account.getUsername());
username.setText(formattedUsername);
int avatarRadius = avatar.getContext().getResources()
.getDimensionPixelSize(R.dimen.avatar_radius_48dp);
ImageLoadingHelper.loadAvatar(account.getAvatar(), avatar, avatarRadius, animateAvatar);
}

void setupActionListener(final AccountActionListener listener) {
accept.setOnClickListener(v -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onRespondToFollowRequest(true, id, position);
}
});
reject.setOnClickListener(v -> {
int position = getAdapterPosition();
if (position != RecyclerView.NO_POSITION) {
listener.onRespondToFollowRequest(false, id, position);
}
});
avatar.setOnClickListener(v -> listener.onViewAccount(id));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import com.keylesspalace.tusky.entity.Account;
import com.keylesspalace.tusky.entity.Emoji;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.interfaces.LinkListener;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
import com.keylesspalace.tusky.util.CardViewMode;
Expand Down Expand Up @@ -72,8 +73,9 @@ public interface AdapterDataSource<T> {
private static final int VIEW_TYPE_STATUS = 0;
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 1;
private static final int VIEW_TYPE_FOLLOW = 2;
private static final int VIEW_TYPE_PLACEHOLDER = 3;
private static final int VIEW_TYPE_UNKNOWN = 4;
private static final int VIEW_TYPE_FOLLOW_REQUEST = 3;
private static final int VIEW_TYPE_PLACEHOLDER = 4;
private static final int VIEW_TYPE_UNKNOWN = 5;

private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE};
private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0];
Expand All @@ -82,20 +84,23 @@ public interface AdapterDataSource<T> {
private StatusDisplayOptions statusDisplayOptions;
private StatusActionListener statusListener;
private NotificationActionListener notificationActionListener;
private AccountActionListener accountActionListener;
private BidiFormatter bidiFormatter;
private AdapterDataSource<NotificationViewData> dataSource;

public NotificationsAdapter(String accountId,
AdapterDataSource<NotificationViewData> dataSource,
StatusDisplayOptions statusDisplayOptions,
StatusActionListener statusListener,
NotificationActionListener notificationActionListener) {
NotificationActionListener notificationActionListener,
AccountActionListener accountActionListener) {

this.accountId = accountId;
this.dataSource = dataSource;
this.statusDisplayOptions = statusDisplayOptions;
this.statusListener = statusListener;
this.notificationActionListener = notificationActionListener;
this.accountActionListener = accountActionListener;
bidiFormatter = BidiFormatter.getInstance();
}

Expand All @@ -119,6 +124,11 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int
.inflate(R.layout.item_follow, parent, false);
return new FollowViewHolder(view, statusDisplayOptions);
}
case VIEW_TYPE_FOLLOW_REQUEST: {
View view = inflater
.inflate(R.layout.item_follow_request, parent, false);
return new FollowRequestViewHolder(view, true);
}
case VIEW_TYPE_PLACEHOLDER: {
View view = inflater
.inflate(R.layout.item_status_placeholder, parent, false);
Expand Down Expand Up @@ -215,6 +225,13 @@ private void bindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int pos
}
break;
}
case VIEW_TYPE_FOLLOW_REQUEST: {
if (payloadForHolder == null) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(concreteNotificaton.getAccount(), bidiFormatter);
holder.setupActionListener(accountActionListener);
}
}
default:
}
}
Expand Down Expand Up @@ -258,6 +275,9 @@ public int getItemViewType(int position) {
case FOLLOW: {
return VIEW_TYPE_FOLLOW;
}
case FOLLOW_REQUEST: {
return VIEW_TYPE_FOLLOW_REQUEST;
}
default: {
return VIEW_TYPE_UNKNOWN;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
var notificationsEnabled: Boolean = true,
var notificationsMentioned: Boolean = true,
var notificationsFollowed: Boolean = true,
var notificationsFollowRequested: Boolean = false,
var notificationsReblogged: Boolean = true,
var notificationsFavorited: Boolean = true,
var notificationsPolls: Boolean = true,
Expand Down
11 changes: 9 additions & 2 deletions app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class
}, version = 21)
}, version = 22)
public abstract class AppDatabase extends RoomDatabase {

public abstract TootDao tootDao();
Expand Down Expand Up @@ -326,4 +326,11 @@ public void migrate(@NonNull SupportSQLiteDatabase database) {
}
};

}
public static final Migration MIGRATION_21_22 = new Migration(21, 22) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFollowRequested` INTEGER NOT NULL DEFAULT 0");
}
};

}
2 changes: 1 addition & 1 deletion app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class AppModule {
AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13,
AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16,
AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19,
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21)
AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22)
.build()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ data class Notification(
REBLOG("reblog"),
FAVOURITE("favourite"),
FOLLOW("follow"),
FOLLOW_REQUEST("follow_request"),
POLL("poll");

companion object {
Expand All @@ -43,7 +44,7 @@ data class Notification(
}
return UNKNOWN
}
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, POLL)
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL)
}

override fun toString(): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.Notification;
import com.keylesspalace.tusky.entity.Poll;
import com.keylesspalace.tusky.entity.Relationship;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.AccountActionListener;
import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
import com.keylesspalace.tusky.interfaces.ReselectableFragment;
import com.keylesspalace.tusky.interfaces.StatusActionListener;
Expand Down Expand Up @@ -115,6 +117,7 @@ public class NotificationsFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener,
StatusActionListener,
NotificationsAdapter.NotificationActionListener,
AccountActionListener,
Injectable, ReselectableFragment {
private static final String TAG = "NotificationF"; // logging tag

Expand Down Expand Up @@ -251,7 +254,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
);

adapter = new NotificationsAdapter(accountManager.getActiveAccount().getAccountId(),
dataSource, statusDisplayOptions, this, this);
dataSource, statusDisplayOptions, this, this, this);
alwaysShowSensitiveMedia = accountManager.getActiveAccount().getAlwaysShowSensitiveMedia();
alwaysOpenSpoiler = accountManager.getActiveAccount().getAlwaysOpenSpoiler();
recyclerView.setAdapter(adapter);
Expand Down Expand Up @@ -765,6 +768,8 @@ private String getNotificationText(Notification.Type type) {
return getString(R.string.notification_boost_name);
case FOLLOW:
return getString(R.string.notification_follow_name);
case FOLLOW_REQUEST:
return getString(R.string.notification_follow_request_name);
case POLL:
return getString(R.string.notification_poll_name);
default:
Expand Down Expand Up @@ -817,6 +822,50 @@ public void onViewAccount(String id) {
super.viewAccount(id);
}

@Override
public void onMute(boolean mute, String id, int position) {
// No muting from notifications yet
}

@Override
public void onBlock(boolean block, String id, int position) {
// No blocking from notifications yet
}

@Override
public void onRespondToFollowRequest(boolean accept, String id, int position) {
Callback<Relationship> callback = new Callback<Relationship>() {
@Override
public void onResponse(Call<Relationship> call, Response<Relationship> response) {
if (response.isSuccessful()) {
onRespondToFollowRequestSuccess();
} else {
onRespondToFollowRequestFailure(accept, id);
}
}

@Override
public void onFailure(Call<Relationship> call, Throwable t) {
onRespondToFollowRequestFailure(accept, id);
}
};

if (accept) {
mastodonApi.authorizeFollowRequest(id).enqueue(callback);
} else {
mastodonApi.rejectFollowRequest(id).enqueue(callback);
}
}

private void onRespondToFollowRequestSuccess() {
fullyRefreshWithProgressBar(true);
}

private void onRespondToFollowRequestFailure(boolean accept, String accountId) {
String verb = accept ? "accept" : "reject";
Log.e(TAG, String.format("Failed to %s account id %s", verb, accountId));
}

@Override
public void onViewStatusForNotificationId(String notificationId) {
for (Either<Placeholder, Notification> either : notifications) {
Expand Down
Loading

0 comments on commit 736d1ca

Please sign in to comment.