Add notifications for follow requests (#1729)

* Add notifications for follow requests
Issue #1719

* Revert item_follow_request layout, create new layout for follow request notifications

* Migrate follow request interaction from notification to observable pattern

* Filter follow request notifications by default

* Add missing cases for system notification generation

* Format code
main
Levi Bard 4 years ago committed by Alibek Omarov
parent 333c906a36
commit 507d9fe19d
  1. 759
      app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json
  2. 51
      app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt
  3. 60
      app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java
  4. 22
      app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java
  5. 3
      app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt
  6. 10
      app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java
  7. 2
      app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt
  8. 5
      app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt
  9. 34
      app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java
  10. 59
      app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt
  11. 10
      app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt
  12. 14
      app/src/main/java/com/keylesspalace/tusky/util/NotificationHelper.java
  13. 96
      app/src/main/res/layout/item_follow_request_notification.xml
  14. 4
      app/src/main/res/values/strings.xml
  15. 6
      app/src/main/res/xml/notification_preferences.xml
  16. 1
      app/src/test/java/com/keylesspalace/tusky/ComposeActivityTest.kt

@ -0,0 +1,759 @@
{
"formatVersion": 1,
"database": {
"version": 24,
"identityHash": "90a7a3288df43c1f177c54c013629b0f",
"entities": [
{
"tableName": "TootEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT, `formattingSyntax` TEXT NOT NULL, `markdownMode` INTEGER)",
"fields": [
{
"fieldPath": "uid",
"columnName": "uid",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "urls",
"columnName": "urls",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "descriptions",
"columnName": "descriptions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "contentWarning",
"columnName": "contentWarning",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToId",
"columnName": "inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToText",
"columnName": "inReplyToText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToUsername",
"columnName": "inReplyToUsername",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "formattingSyntax",
"columnName": "formattingSyntax",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "markdownMode",
"columnName": "markdownMode",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"uid"
],
"autoGenerate": true
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "AccountEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsEmojiReactions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `defaultFormattingSyntax` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "domain",
"columnName": "domain",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accessToken",
"columnName": "accessToken",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isActive",
"columnName": "isActive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "profilePictureUrl",
"columnName": "profilePictureUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsEnabled",
"columnName": "notificationsEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsMentioned",
"columnName": "notificationsMentioned",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowed",
"columnName": "notificationsFollowed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFollowRequested",
"columnName": "notificationsFollowRequested",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsReblogged",
"columnName": "notificationsReblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsFavorited",
"columnName": "notificationsFavorited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsPolls",
"columnName": "notificationsPolls",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationsEmojiReactions",
"columnName": "notificationsEmojiReactions",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationSound",
"columnName": "notificationSound",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationVibration",
"columnName": "notificationVibration",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "notificationLight",
"columnName": "notificationLight",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultPostPrivacy",
"columnName": "defaultPostPrivacy",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "defaultMediaSensitivity",
"columnName": "defaultMediaSensitivity",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysShowSensitiveMedia",
"columnName": "alwaysShowSensitiveMedia",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "alwaysOpenSpoiler",
"columnName": "alwaysOpenSpoiler",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaPreviewEnabled",
"columnName": "mediaPreviewEnabled",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastNotificationId",
"columnName": "lastNotificationId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "activeNotifications",
"columnName": "activeNotifications",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "tabPreferences",
"columnName": "tabPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "notificationsFilter",
"columnName": "notificationsFilter",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "defaultFormattingSyntax",
"columnName": "defaultFormattingSyntax",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"id"
],
"autoGenerate": true
},
"indices": [
{
"name": "index_AccountEntity_domain_accountId",
"unique": true,
"columnNames": [
"domain",
"accountId"
],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)"
}
],
"foreignKeys": []
},
{
"tableName": "InstanceEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))",
"fields": [
{
"fieldPath": "instance",
"columnName": "instance",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojiList",
"columnName": "emojiList",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "maximumTootCharacters",
"columnName": "maximumTootCharacters",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptions",
"columnName": "maxPollOptions",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "maxPollOptionLength",
"columnName": "maxPollOptionLength",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "version",
"columnName": "version",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"instance"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "TimelineStatusEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )",
"fields": [
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timelineUserId",
"columnName": "timelineUserId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "authorServerId",
"columnName": "authorServerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToId",
"columnName": "inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "inReplyToAccountId",
"columnName": "inReplyToAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogsCount",
"columnName": "reblogsCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favouritesCount",
"columnName": "favouritesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "reblogged",
"columnName": "reblogged",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookmarked",
"columnName": "bookmarked",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "favourited",
"columnName": "favourited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sensitive",
"columnName": "sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "spoilerText",
"columnName": "spoilerText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "visibility",
"columnName": "visibility",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "mentions",
"columnName": "mentions",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "application",
"columnName": "application",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogServerId",
"columnName": "reblogServerId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "reblogAccountId",
"columnName": "reblogAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "poll",
"columnName": "poll",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"serverId",
"timelineUserId"
],
"autoGenerate": false
},
"indices": [
{
"name": "index_TimelineStatusEntity_authorServerId_timelineUserId",
"unique": false,
"columnNames": [
"authorServerId",
"timelineUserId"
],
"createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)"
}
],
"foreignKeys": [
{
"table": "TimelineAccountEntity",
"onDelete": "NO ACTION",
"onUpdate": "NO ACTION",
"columns": [
"authorServerId",
"timelineUserId"
],
"referencedColumns": [
"serverId",
"timelineUserId"
]
}
]
},
{
"tableName": "TimelineAccountEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))",
"fields": [
{
"fieldPath": "serverId",
"columnName": "serverId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "timelineUserId",
"columnName": "timelineUserId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "localUsername",
"columnName": "localUsername",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "username",
"columnName": "username",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "displayName",
"columnName": "displayName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "avatar",
"columnName": "avatar",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "emojis",
"columnName": "emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "bot",
"columnName": "bot",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"columnNames": [
"serverId",
"timelineUserId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "ConversationEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))",
"fields": [
{
"fieldPath": "accountId",
"columnName": "accountId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "accounts",
"columnName": "accounts",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "unread",
"columnName": "unread",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.id",
"columnName": "s_id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.url",
"columnName": "s_url",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastStatus.inReplyToId",
"columnName": "s_inReplyToId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastStatus.inReplyToAccountId",
"columnName": "s_inReplyToAccountId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lastStatus.account",
"columnName": "s_account",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.content",
"columnName": "s_content",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.createdAt",
"columnName": "s_createdAt",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.emojis",
"columnName": "s_emojis",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.favouritesCount",
"columnName": "s_favouritesCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.favourited",
"columnName": "s_favourited",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.bookmarked",
"columnName": "s_bookmarked",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.sensitive",
"columnName": "s_sensitive",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.spoilerText",
"columnName": "s_spoilerText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.attachments",
"columnName": "s_attachments",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.mentions",
"columnName": "s_mentions",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastStatus.showingHiddenContent",
"columnName": "s_showingHiddenContent",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.expanded",
"columnName": "s_expanded",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.collapsible",
"columnName": "s_collapsible",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.collapsed",
"columnName": "s_collapsed",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastStatus.poll",
"columnName": "s_poll",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"columnNames": [
"id",
"accountId"
],
"autoGenerate": false
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '90a7a3288df43c1f177c54c013629b0f')"
]
}
}

@ -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_notification.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) }
}
}

@ -18,19 +18,12 @@ package com.keylesspalace.tusky.adapter;
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 {
@ -46,7 +39,7 @@ public class FollowRequestsAdapter extends AccountAdapter {
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())
@ -60,57 +53,8 @@ public class FollowRequestsAdapter extends AccountAdapter {
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));
}
}
}

@ -42,6 +42,7 @@ import com.keylesspalace.tusky.R;
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;
@ -74,6 +75,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private static final int VIEW_TYPE_FOLLOW = 2;
private static final int VIEW_TYPE_PLACEHOLDER = 3;
private static final int VIEW_TYPE_MUTED_STATUS = 4;
private static final int VIEW_TYPE_FOLLOW_REQUEST = 5;
private static final int VIEW_TYPE_UNKNOWN = 6;
private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE};
@ -83,6 +85,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
private StatusDisplayOptions statusDisplayOptions;
private StatusActionListener statusListener;
private NotificationActionListener notificationActionListener;
private AccountActionListener accountActionListener;
private BidiFormatter bidiFormatter;
private AdapterDataSource<NotificationViewData> dataSource;
@ -90,13 +93,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
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();
}
@ -125,6 +130,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
.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_notification, parent, false);
return new FollowRequestViewHolder(view, true);
}
case VIEW_TYPE_PLACEHOLDER: {
View view = inflater
.inflate(R.layout.item_status_placeholder, parent, false);
@ -228,6 +238,13 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
}
break;
}
case VIEW_TYPE_FOLLOW_REQUEST: {
if (payloadForHolder == null) {
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
holder.setupWithAccount(concreteNotificaton.getAccount(), bidiFormatter);
holder.setupActionListener(accountActionListener);
}
}
default:
}
}
@ -274,6 +291,9 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
case FOLLOW: {
return VIEW_TYPE_FOLLOW;
}
case FOLLOW_REQUEST: {
return VIEW_TYPE_FOLLOW_REQUEST;
}
default: {
return VIEW_TYPE_UNKNOWN;
}

@ -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,
@ -55,7 +56,7 @@ data class AccountEntity(@field:PrimaryKey(autoGenerate = true) var id: Long,
var activeNotifications: String = "[]",
var emojis: List<Emoji> = emptyList(),
var tabPreferences: List<TabData> = defaultTabs(),
var notificationsFilter: String = "[]",
var notificationsFilter: String = "[\"follow_request\"]",
var defaultFormattingSyntax: String = "") {
val identifier: String

@ -29,8 +29,7 @@ import androidx.annotation.NonNull;
*/
@Database(entities = {TootEntity.class, AccountEntity.class, InstanceEntity.class, TimelineStatusEntity.class,
TimelineAccountEntity.class, ConversationEntity.class
}, version = 23)
TimelineAccountEntity.class, ConversationEntity.class}, version = 24)
public abstract class AppDatabase extends RoomDatabase {
public abstract TootDao tootDao();
@ -343,4 +342,11 @@ public abstract class AppDatabase extends RoomDatabase {
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultFormattingSyntax` TEXT NOT NULL DEFAULT ''");
}
};
public static final Migration MIGRATION_23_24 = new Migration(23, 24) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFollowRequested` INTEGER NOT NULL DEFAULT 0");
}
};
}

@ -80,7 +80,7 @@ class AppModule {
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_21_22,
AppDatabase.MIGRATION_22_23)
AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24)
.build()
}

@ -39,7 +39,8 @@ data class Notification(
FAVOURITE("favourite"),
FOLLOW("follow"),
POLL("poll"),
EMOJI_REACTION("pleroma:emoji_reaction");
EMOJI_REACTION("pleroma:emoji_reaction"),
FOLLOW_REQUEST("follow_request");
companion object {
@ -51,7 +52,7 @@ data class Notification(
}
return UNKNOWN
}
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, POLL, EMOJI_REACTION)
val asList = listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, POLL, EMOJI_REACTION, FOLLOW_REQUEST)
}
override fun toString(): String {

@ -59,6 +59,11 @@ import com.keylesspalace.tusky.db.AccountEntity;
import com.keylesspalace.tusky.db.AccountManager;
import com.keylesspalace.tusky.di.Injectable;
import com.keylesspalace.tusky.entity.*;
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;
@ -91,6 +96,7 @@ import javax.inject.Inject;
import at.connyduck.sparkbutton.helpers.Utils;
import io.reactivex.Observable;
import io.reactivex.Single;
import io.reactivex.android.schedulers.AndroidSchedulers;
import kotlin.Unit;
import kotlin.collections.CollectionsKt;
@ -108,6 +114,7 @@ public class NotificationsFragment extends SFragment implements
SwipeRefreshLayout.OnRefreshListener,
StatusActionListener,
NotificationsAdapter.NotificationActionListener,
AccountActionListener,
Injectable, ReselectableFragment {
private static final String TAG = "NotificationF"; // logging tag
@ -244,7 +251,7 @@ public class NotificationsFragment extends SFragment implements
);
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);
@ -818,6 +825,8 @@ public class NotificationsFragment extends SFragment implements
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:
@ -870,6 +879,29 @@ public class NotificationsFragment extends SFragment implements
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) {
Single<Relationship> request = accept ?
mastodonApi.authorizeFollowRequestObservable(id) :
mastodonApi.rejectFollowRequestObservable(id);
request.observeOn(AndroidSchedulers.mainThread())
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
.subscribe(
(relationship) -> fullyRefreshWithProgressBar(true),
(error) -> Log.e(TAG, String.format("Failed to %s account id %s", accept ? "accept" : "reject", id))
);
}
@Override
public void onViewStatusForNotificationId(String notificationId) {
for (Either<Placeholder, Notification> either : notifications) {

@ -41,46 +41,24 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Preference.O
val activeAccount = accountManager.activeAccount
if (activeAccount != null) {
val notificationPref = requirePreference("notificationsEnabled") as SwitchPreferenceCompat
notificationPref.isChecked = activeAccount.notificationsEnabled
notificationPref.onPreferenceChangeListener = this
val mentionedPref = requirePreference("notificationFilterMentions") as SwitchPreferenceCompat
mentionedPref.isChecked = activeAccount.notificationsMentioned
mentionedPref.onPreferenceChangeListener = this
val followedPref = requirePreference("notificationFilterFollows") as SwitchPreferenceCompat
followedPref.isChecked = activeAccount.notificationsFollowed
followedPref.onPreferenceChangeListener = this
val boostedPref = requirePreference("notificationFilterReblogs") as SwitchPreferenceCompat
boostedPref.isChecked = activeAccount.notificationsReblogged
boostedPref.onPreferenceChangeListener = this
val favoritedPref = requirePreference("notificationFilterFavourites") as SwitchPreferenceCompat
favoritedPref.isChecked = activeAccount.notificationsFavorited
favoritedPref.onPreferenceChangeListener = this
val pollsPref = requirePreference("notificationFilterPolls") as SwitchPreferenceCompat
pollsPref.isChecked = activeAccount.notificationsPolls
pollsPref.onPreferenceChangeListener = this
val emojisPref = requirePreference("notificationFilterEmojis") as SwitchPreferenceCompat
emojisPref.isChecked = activeAccount.notificationsEmojiReactions
emojisPref.onPreferenceChangeListener = this
val soundPref = requirePreference("notificationAlertSound") as SwitchPreferenceCompat
soundPref.isChecked = activeAccount.notificationSound
soundPref.onPreferenceChangeListener = this
val vibrationPref = requirePreference("notificationAlertVibrate") as SwitchPreferenceCompat
vibrationPref.isChecked = activeAccount.notificationVibration
vibrationPref.onPreferenceChangeListener = this
val lightPref = requirePreference("notificationAlertLight") as SwitchPreferenceCompat
lightPref.isChecked = activeAccount.notificationLight
lightPref.onPreferenceChangeListener = this
for (pair in mapOf(
"notificationsEnabled" to activeAccount.notificationsEnabled,
"notificationFilterMentions" to activeAccount.notificationsMentioned,
"notificationFilterFollows" to activeAccount.notificationsFollowed,
"notificationFilterFollowRequests" to activeAccount.notificationsFollowRequested,
"notificationFilterReblogs" to activeAccount.notificationsReblogged,
"notificationFilterFavourites" to activeAccount.notificationsFavorited,
"notificationFilterPolls" to activeAccount.notificationsPolls,
"notificationFilterEmojis" to activeAccount.notificationsEmojiReactions,
"notificationAlertSound" to activeAccount.notificationSound,
"notificationAlertVibrate" to activeAccount.notificationVibration,
"notificationAlertLight" to activeAccount.notificationLight
)) {
(requirePreference(pair.key) as SwitchPreferenceCompat).apply {
isChecked = pair.value
onPreferenceChangeListener = this@NotificationPreferencesFragment
}
}
}
}
@ -100,6 +78,7 @@ class NotificationPreferencesFragment : PreferenceFragmentCompat(), Preference.O
}
"notificationFilterMentions" -> activeAccount.notificationsMentioned = newValue as Boolean
"notificationFilterFollows" -> activeAccount.notificationsFollowed = newValue as Boolean
"notificationFilterFollowRequests" -> activeAccount.notificationsFollowRequested = newValue as Boolean
"notificationFilterReblogs" -> activeAccount.notificationsReblogged = newValue as Boolean
"notificationFilterFavourites" -> activeAccount.notificationsFavorited = newValue as Boolean
"notificationFilterPolls" -> activeAccount.notificationsPolls = newValue as Boolean

@ -413,6 +413,16 @@ interface MastodonApi {
@Path("id") accountId: String
): Call<Relationship>
@POST("api/v1/follow_requests/{id}/authorize")
fun authorizeFollowRequestObservable(
@Path("id") accountId: String
): Single<Relationship>
@POST("api/v1/follow_requests/{id}/reject")
fun rejectFollowRequestObservable(
@Path("id") accountId: String
): Single<Relationship>
@FormUrlEncoded
@POST("api/v1/apps")
fun authenticateApp(

@ -113,6 +113,7 @@ public class NotificationHelper {
**/
public static final String CHANNEL_MENTION = "CHANNEL_MENTION";
public static final String CHANNEL_FOLLOW = "CHANNEL_FOLLOW";
public static final String CHANNEL_FOLLOW_REQUEST = "CHANNEL_FOLLOW_REQUEST";
public static final String CHANNEL_BOOST = "CHANNEL_BOOST";
public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE";
public static final String CHANNEL_POLL = "CHANNEL_POLL";
@ -366,6 +367,7 @@ public class NotificationHelper {
String[] channelIds = new String[]{
CHANNEL_MENTION + account.getIdentifier(),
CHANNEL_FOLLOW + account.getIdentifier(),
CHANNEL_FOLLOW_REQUEST + account.getIdentifier(),
CHANNEL_BOOST + account.getIdentifier(),
CHANNEL_FAVOURITE + account.getIdentifier(),
CHANNEL_POLL + account.getIdentifier(),
@ -374,6 +376,7 @@ public class NotificationHelper {
int[] channelNames = {
R.string.notification_mention_name,
R.string.notification_follow_name,
R.string.notification_follow_request_name,
R.string.notification_boost_name,
R.string.notification_favourite_name,
R.string.notification_poll_name,
@ -382,13 +385,14 @@ public class NotificationHelper {
int[] channelDescriptions = {
R.string.notification_mention_descriptions,
R.string.notification_follow_description,
R.string.notification_follow_request_description,
R.string.notification_boost_description,
R.string.notification_favourite_description,
R.string.notification_poll_description,
R.string.notification_emoji_description
};
List<NotificationChannel> channels = new ArrayList<>(5);
List<NotificationChannel> channels = new ArrayList<>(6);
NotificationChannelGroup channelGroup = new NotificationChannelGroup(account.getIdentifier(), account.getFullName());
@ -529,6 +533,8 @@ public class NotificationHelper {
return account.getNotificationsMentioned();
case FOLLOW:
return account.getNotificationsFollowed();
case FOLLOW_REQUEST:
return account.getNotificationsFollowRequested();
case REBLOG:
return account.getNotificationsReblogged();
case FAVOURITE:
@ -548,6 +554,8 @@ public class NotificationHelper {
return CHANNEL_MENTION + account.getIdentifier();
case FOLLOW:
return CHANNEL_FOLLOW + account.getIdentifier();
case FOLLOW_REQUEST:
return CHANNEL_FOLLOW_REQUEST + account.getIdentifier();
case REBLOG:
return CHANNEL_BOOST + account.getIdentifier();
case FAVOURITE:
@ -619,6 +627,9 @@ public class NotificationHelper {
case FOLLOW:
return String.format(context.getString(R.string.notification_follow_format),
accountName);
case FOLLOW_REQUEST:
return String.format(context.getString(R.string.notification_follow_request_format),
accountName);
case FAVOURITE:
return String.format(context.getString(R.string.notification_favourite_format),
accountName);
@ -641,6 +652,7 @@ public class NotificationHelper {
private static String bodyForType(Notification notification, Context context) {
switch (notification.getType()) {
case FOLLOW:
case FOLLOW_REQUEST:
return "@" + notification.getAccount().getUsername();
case MENTION:
case FAVOURITE:

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:paddingBottom="10dp">
<androidx.emoji.widget.EmojiTextView
android:id="@+id/notificationTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:drawableStart="@drawable/ic_person_add_24dp"
android:drawablePadding="10dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingStart="28dp"
android:textColor="?android:textColorTertiary"
android:textSize="?attr/status_text_medium"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Someone requested to follow you" />
<ImageView
android:id="@+id/avatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_centerVertical="true"
android:layout_marginTop="10dp"
android:contentDescription="@string/action_view_profile"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/notificationTextView" />
<androidx.emoji.widget.EmojiTextView
android:id="@+id/displayNameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginTop="6dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorPrimary"
android:textSize="?attr/status_text_large"
android:textStyle="normal|bold"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toBottomOf="@id/notificationTextView"
tools:text="Display name" />
<TextView
android:id="@+id/usernameTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:textColorSecondary"
android:textSize="?attr/status_text_medium"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toBottomOf="@id/displayNameTextView"
tools:text="\@username" />
<ImageButton
android:id="@+id/acceptButton"
style="@style/TuskyImageButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerVertical="true"
android:layout_marginStart="12dp"
android:layout_marginTop="14dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_accept"
android:padding="4dp"
app:layout_constraintEnd_toStartOf="@id/rejectButton"
app:layout_constraintTop_toBottomOf="@id/notificationTextView"
app:srcCompat="@drawable/ic_check_24dp" />
<ImageButton
android:id="@+id/rejectButton"
style="@style/TuskyImageButton"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_centerVertical="true"
android:layout_marginStart="12dp"
android:layout_marginTop="14dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_reject"
android:padding="4dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/notificationTextView"
app:srcCompat="@drawable/ic_reject_24dp" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -60,6 +60,7 @@
<string name="notification_reblog_format">%s reposted your post</string>
<string name="notification_favourite_format">%s favorited your post</string>
<string name="notification_follow_format">%s followed you</string>
<string name="notification_follow_request_format">%s requested to follow you</string>
<string name="report_username_format">Report @%s</string>
<string name="report_comment_hint">Additional comments?</string>
@ -209,6 +210,7 @@
<string name="pref_title_notification_filter_mentions">mentioned</string>
<string name="pref_title_notification_filter_follows">followed</string>
<string name="pref_title_notification_filter_reblogs">my posts are reposted</string>
<string name="pref_title_notification_filter_follow_requests">follow requested</string>
<string name="pref_title_notification_filter_favourites">my posts are favorited</string>
<string name="pref_title_notification_filter_poll">polls have ended</string>
<string name="pref_title_appearance_settings">Appearance</string>
@ -264,6 +266,8 @@
<string name="notification_follow_description">Notifications about new followers</string>
<string name="notification_boost_name">Reposts</string>
<string name="notification_boost_description">Notifications when your posts get reposted</string>
<string name="notification_follow_request_name">Follow Requests</string>
<string name="notification_follow_request_description">Notifications about follow requests</string>
<string name="notification_favourite_name">Favorites</string>
<string name="notification_favourite_description">Notifications when your posts get marked as favorite</string>
<string name="notification_poll_name">Polls</string>

@ -27,6 +27,12 @@
android:title="@string/pref_title_notification_filter_follows"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="notificationFilterFollowRequests"
android:title="@string/pref_title_notification_filter_follow_requests"
app:iconSpaceReserved="false" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="notificationFilterReblogs"

@ -67,6 +67,7 @@ class ComposeActivityTest {
notificationsEnabled = true,
notificationsMentioned = true,
notificationsFollowed = true,
notificationsFollowRequested = false,
notificationsReblogged = true,
notificationsFavorited = true,
notificationSound = true,

Loading…
Cancel
Save