From 507d9fe19d0b7ce7bcd6ec2eae925ee184fddf96 Mon Sep 17 00:00:00 2001 From: Levi Bard Date: Thu, 19 Mar 2020 22:02:10 +0100 Subject: [PATCH] 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 --- .../24.json | 759 ++++++++++++++++++ .../tusky/adapter/FollowRequestViewHolder.kt | 51 ++ .../tusky/adapter/FollowRequestsAdapter.java | 60 +- .../tusky/adapter/NotificationsAdapter.java | 22 +- .../keylesspalace/tusky/db/AccountEntity.kt | 3 +- .../keylesspalace/tusky/db/AppDatabase.java | 10 +- .../com/keylesspalace/tusky/di/AppModule.kt | 2 +- .../tusky/entity/Notification.kt | 5 +- .../tusky/fragment/NotificationsFragment.java | 34 +- .../NotificationPreferencesFragment.kt | 59 +- .../tusky/network/MastodonApi.kt | 10 + .../tusky/util/NotificationHelper.java | 14 +- .../item_follow_request_notification.xml | 96 +++ app/src/main/res/values/strings.xml | 4 + .../main/res/xml/notification_preferences.xml | 6 + .../tusky/ComposeActivityTest.kt | 1 + 16 files changed, 1029 insertions(+), 107 deletions(-) create mode 100644 app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json create mode 100644 app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt create mode 100644 app/src/main/res/layout/item_follow_request_notification.xml diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json new file mode 100644 index 00000000..7c98d371 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt new file mode 100644 index 00000000..d9fd0491 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt @@ -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) } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java index 37f09f01..548d893c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestsAdapter.java @@ -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)); - } - } } diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index 6c6d55af..ee746891 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -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 dataSource; @@ -90,13 +93,15 @@ public class NotificationsAdapter extends RecyclerView.Adapter { AdapterDataSource 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; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt index de076505..9913fd23 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountEntity.kt @@ -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 = emptyList(), var tabPreferences: List = defaultTabs(), - var notificationsFilter: String = "[]", + var notificationsFilter: String = "[\"follow_request\"]", var defaultFormattingSyntax: String = "") { val identifier: String diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java index aef669e0..5dff6e70 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -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"); + } + }; } diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index fb1cc9fa..5cf4af5a 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -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() } diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt index 0ee84e78..2fbf4d4b 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt @@ -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 { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java index 9bac1b45..9259f4a6 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/NotificationsFragment.java @@ -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 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 either : notifications) { diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt index 5f3809a4..c8df4785 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/preference/NotificationPreferencesFragment.kt @@ -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 diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt index 7cdc93a9..4e12a853 100644 --- a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -413,6 +413,16 @@ interface MastodonApi { @Path("id") accountId: String ): Call + @POST("api/v1/follow_requests/{id}/authorize") + fun authorizeFollowRequestObservable( + @Path("id") accountId: String + ): Single + + @POST("api/v1/follow_requests/{id}/reject") + fun rejectFollowRequestObservable( + @Path("id") accountId: String + ): Single + @FormUrlEncoded @POST("api/v1/apps") fun authenticateApp( diff --git a/app/src/main/java/com/keylesspalace/tusky/util/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/util/NotificationHelper.java index 4882693f..e17b19ac 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/NotificationHelper.java +++ b/app/src/main/java/com/keylesspalace/tusky/util/NotificationHelper.java @@ -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 channels = new ArrayList<>(5); + List 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: diff --git a/app/src/main/res/layout/item_follow_request_notification.xml b/app/src/main/res/layout/item_follow_request_notification.xml new file mode 100644 index 00000000..712c08d4 --- /dev/null +++ b/app/src/main/res/layout/item_follow_request_notification.xml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8f615493..01233d39 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -60,6 +60,7 @@ %s reposted your post %s favorited your post %s followed you + %s requested to follow you Report @%s Additional comments? @@ -209,6 +210,7 @@ mentioned followed my posts are reposted + follow requested my posts are favorited polls have ended Appearance @@ -264,6 +266,8 @@ Notifications about new followers Reposts Notifications when your posts get reposted + Follow Requests + Notifications about follow requests Favorites Notifications when your posts get marked as favorite Polls diff --git a/app/src/main/res/xml/notification_preferences.xml b/app/src/main/res/xml/notification_preferences.xml index 120e6cbf..fd590f9c 100644 --- a/app/src/main/res/xml/notification_preferences.xml +++ b/app/src/main/res/xml/notification_preferences.xml @@ -27,6 +27,12 @@ android:title="@string/pref_title_notification_filter_follows" app:iconSpaceReserved="false" /> + +