From b95cb2bedbdc79d4189fbfd6f3818dc5619515a1 Mon Sep 17 00:00:00 2001
From: Harris <hjubb@users.noreply.github.com>
Date: Tue, 20 Jul 2021 16:06:59 +1000
Subject: [PATCH 1/5] feat: add notification types for all / mentions only or
 none with UI and push logic

---
 .../thoughtcrime/securesms/MuteDialog.java    |  4 +-
 .../v2/menus/ConversationMenuHelper.kt        | 13 ++++-
 .../v2/utilities/NotificationUtils.kt         | 21 +++++++++
 .../securesms/database/RecipientDatabase.java | 39 ++++++++++++---
 .../database/helpers/SQLCipherOpenHelper.java | 21 +++++----
 .../home/ConversationOptionsBottomSheet.kt    | 33 ++++++++++---
 .../securesms/home/HomeActivity.kt            | 47 +++++++++++++++++--
 .../notifications/DefaultMessageNotifier.java | 21 ++++++---
 .../ic_outline_notification_important_24.xml  | 10 ++++
 .../ic_outline_notifications_active_24.xml    | 10 ++++
 .../fragment_conversation_bottom_sheet.xml    | 27 +++++++++++
 .../menu/menu_conversation_closed_group.xml   |  4 ++
 .../res/menu/menu_conversation_open_group.xml |  4 ++
 app/src/main/res/values/arrays.xml            |  7 ++-
 app/src/main/res/values/strings.xml           |  5 ++
 .../utilities/recipients/Recipient.java       | 40 +++++++++++-----
 .../recipients/RecipientProvider.java         | 17 +++----
 17 files changed, 267 insertions(+), 56 deletions(-)
 create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt
 create mode 100644 app/src/main/res/drawable/ic_outline_notification_important_24.xml
 create mode 100644 app/src/main/res/drawable/ic_outline_notifications_active_24.xml

diff --git a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.java b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.java
index 33aace5123..acca9f8375 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/MuteDialog.java
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms;
 
 import android.content.Context;
 import android.content.DialogInterface;
+
 import androidx.annotation.NonNull;
 import androidx.appcompat.app.AlertDialog;
 
@@ -33,11 +34,10 @@ public class MuteDialog extends AlertDialog {
         final long muteUntil;
 
         switch (which) {
-          case 0:  muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1);  break;
           case 1:  muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(2);  break;
           case 2:  muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1);   break;
           case 3:  muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(7);   break;
-          case 4:  muteUntil = System.currentTimeMillis() + TimeUnit.DAYS.toMillis(365); break;
+          case 4:  muteUntil = Long.MAX_VALUE; break;
           default: muteUntil = System.currentTimeMillis() + TimeUnit.HOURS.toMillis(1);  break;
         }
 
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
index 93278cb6a6..713add9277 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuHelper.kt
@@ -36,13 +36,14 @@ import org.session.libsession.utilities.recipients.Recipient
 import org.session.libsignal.utilities.guava.Optional
 import org.session.libsignal.utilities.toHexString
 import org.thoughtcrime.securesms.*
+import org.thoughtcrime.securesms.contacts.SelectContactsActivity
 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
+import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
 import org.thoughtcrime.securesms.database.DatabaseFactory
 import org.thoughtcrime.securesms.groups.EditClosedGroupActivity
 import org.thoughtcrime.securesms.groups.EditClosedGroupActivity.Companion.groupIDKey
-import org.thoughtcrime.securesms.contacts.SelectContactsActivity
-import org.thoughtcrime.securesms.util.getColorWithID
 import org.thoughtcrime.securesms.util.BitmapUtil
+import org.thoughtcrime.securesms.util.getColorWithID
 import java.io.IOException
 
 object ConversationMenuHelper {
@@ -152,6 +153,7 @@ object ConversationMenuHelper {
             R.id.menu_invite_to_open_group -> { inviteContacts(context, thread) }
             R.id.menu_unmute_notifications -> { unmute(context, thread) }
             R.id.menu_mute_notifications -> { mute(context, thread) }
+            R.id.menu_notification_settings -> { setNotifyType(context, thread) }
         }
         return true
     }
@@ -325,4 +327,11 @@ object ConversationMenuHelper {
             DatabaseFactory.getRecipientDatabase(context).setMuted(thread, until)
         }
     }
+
+    private fun setNotifyType(context: Context, thread: Recipient) {
+        NotificationUtils.showNotifyDialog(context, thread) { notifyType ->
+            DatabaseFactory.getRecipientDatabase(context).setNotifyType(thread, notifyType)
+        }
+    }
+
 }
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt
new file mode 100644
index 0000000000..dbbcfb51ef
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/utilities/NotificationUtils.kt
@@ -0,0 +1,21 @@
+package org.thoughtcrime.securesms.conversation.v2.utilities
+
+import android.content.Context
+import androidx.appcompat.app.AlertDialog
+import network.loki.messenger.R
+import org.session.libsession.utilities.recipients.Recipient
+
+object NotificationUtils {
+    fun showNotifyDialog(context: Context, thread: Recipient, notifyTypeHandler: (Int)->Unit) {
+        val notifyTypes = context.resources.getStringArray(R.array.notify_types)
+        val currentSelected = thread.notifyType
+
+        AlertDialog.Builder(context)
+            .setSingleChoiceItems(notifyTypes,currentSelected) { d, newSelection ->
+                notifyTypeHandler(newSelection)
+                d.dismiss()
+            }
+            .setTitle(R.string.RecipientPreferenceActivity_notification_settings)
+            .show()
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
index 287a6d365c..8d7cedb61b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
@@ -10,17 +10,18 @@ import androidx.annotation.Nullable;
 import com.annimon.stream.Stream;
 
 import net.sqlcipher.database.SQLiteDatabase;
-import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
 
-import org.session.libsession.utilities.MaterialColor;
 import org.session.libsession.utilities.Address;
+import org.session.libsession.utilities.MaterialColor;
+import org.session.libsession.utilities.Util;
 import org.session.libsession.utilities.recipients.Recipient;
-import org.session.libsession.utilities.recipients.Recipient.*;
+import org.session.libsession.utilities.recipients.Recipient.RecipientSettings;
+import org.session.libsession.utilities.recipients.Recipient.RegisteredState;
+import org.session.libsession.utilities.recipients.Recipient.UnidentifiedAccessMode;
 import org.session.libsignal.utilities.Base64;
-import org.session.libsession.utilities.Util;
-
-import org.session.libsignal.utilities.guava.Optional;
 import org.session.libsignal.utilities.Log;
+import org.session.libsignal.utilities.guava.Optional;
+import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
 
 import java.io.Closeable;
 import java.io.IOException;
@@ -55,13 +56,14 @@ public class RecipientDatabase extends Database {
   private static final String NOTIFICATION_CHANNEL     = "notification_channel";
   private static final String UNIDENTIFIED_ACCESS_MODE = "unidentified_access_mode";
   private static final String FORCE_SMS_SELECTION      = "force_sms_selection";
+  private static final String NOTIFY_TYPE              = "notify_type"; // all, mentions only, none
 
   private static final String[] RECIPIENT_PROJECTION = new String[] {
       BLOCK, NOTIFICATION, CALL_RINGTONE, VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, EXPIRE_MESSAGES, REGISTERED,
       PROFILE_KEY, SYSTEM_DISPLAY_NAME, SYSTEM_PHOTO_URI, SYSTEM_PHONE_LABEL, SYSTEM_CONTACT_URI,
       SIGNAL_PROFILE_NAME, SIGNAL_PROFILE_AVATAR, PROFILE_SHARING, NOTIFICATION_CHANNEL,
       UNIDENTIFIED_ACCESS_MODE,
-      FORCE_SMS_SELECTION,
+      FORCE_SMS_SELECTION, NOTIFY_TYPE,
   };
 
   static final List<String> TYPED_RECIPIENT_PROJECTION = Stream.of(RECIPIENT_PROJECTION)
@@ -95,6 +97,15 @@ public class RecipientDatabase extends Database {
           UNIDENTIFIED_ACCESS_MODE + " INTEGER DEFAULT 0, " +
           FORCE_SMS_SELECTION + " INTEGER DEFAULT 0);";
 
+  public static String getCreateNotificationTypeCommand() {
+    return "ALTER TABLE "+ TABLE_NAME + " " +
+            "ADD COLUMN " + NOTIFY_TYPE + " INTEGER DEFAULT 0;";
+  }
+
+  public static final int NOTIFY_TYPE_ALL = 0;
+  public static final int NOTIFY_TYPE_MENTIONS = 1;
+  public static final int NOTIFY_TYPE_NONE = 2;
+
   public RecipientDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
     super(context, databaseHelper);
   }
@@ -131,6 +142,7 @@ public class RecipientDatabase extends Database {
     int     messageVibrateState    = cursor.getInt(cursor.getColumnIndexOrThrow(VIBRATE));
     int     callVibrateState       = cursor.getInt(cursor.getColumnIndexOrThrow(CALL_VIBRATE));
     long    muteUntil              = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL));
+    int     notifyType             = cursor.getInt(cursor.getColumnIndexOrThrow(NOTIFY_TYPE));
     String  serializedColor        = cursor.getString(cursor.getColumnIndexOrThrow(COLOR));
     int     defaultSubscriptionId  = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID));
     int     expireMessages         = cursor.getInt(cursor.getColumnIndexOrThrow(EXPIRE_MESSAGES));
@@ -167,6 +179,7 @@ public class RecipientDatabase extends Database {
     }
 
     return Optional.of(new RecipientSettings(blocked, muteUntil,
+                                             notifyType,
                                              Recipient.VibrateState.fromId(messageVibrateState),
                                              Recipient.VibrateState.fromId(callVibrateState),
                                              Util.uri(messageRingtone), Util.uri(callRingtone),
@@ -214,6 +227,18 @@ public class RecipientDatabase extends Database {
     recipient.resolve().setMuted(until);
   }
 
+  /**
+   *
+   * @param recipient to modify notifications for
+   * @param notifyType the new notification type {@link #NOTIFY_TYPE_ALL}, {@link #NOTIFY_TYPE_MENTIONS} or {@link #NOTIFY_TYPE_NONE}
+   */
+  public void setNotifyType(@NonNull Recipient recipient, int notifyType) {
+    ContentValues values = new ContentValues();
+    values.put(NOTIFY_TYPE, notifyType);
+    updateOrInsert(recipient.getAddress(), values);
+    recipient.resolve().setNotifyType(notifyType);
+  }
+
   public void setExpireMessages(@NonNull Recipient recipient, int expiration) {
     recipient.setExpireMessages(expiration);
 
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
index 8c0763d233..efdf55c0b1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
@@ -10,26 +10,26 @@ import net.sqlcipher.database.SQLiteDatabase;
 import net.sqlcipher.database.SQLiteDatabaseHook;
 import net.sqlcipher.database.SQLiteOpenHelper;
 
+import org.session.libsignal.utilities.Log;
 import org.thoughtcrime.securesms.crypto.DatabaseSecret;
 import org.thoughtcrime.securesms.database.AttachmentDatabase;
 import org.thoughtcrime.securesms.database.DraftDatabase;
 import org.thoughtcrime.securesms.database.GroupDatabase;
 import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
 import org.thoughtcrime.securesms.database.JobDatabase;
-import org.thoughtcrime.securesms.database.MmsDatabase;
-import org.thoughtcrime.securesms.database.PushDatabase;
-import org.thoughtcrime.securesms.database.RecipientDatabase;
-import org.thoughtcrime.securesms.database.SearchDatabase;
-import org.thoughtcrime.securesms.database.SmsDatabase;
-import org.thoughtcrime.securesms.database.ThreadDatabase;
-import org.session.libsignal.utilities.Log;
 import org.thoughtcrime.securesms.database.LokiAPIDatabase;
 import org.thoughtcrime.securesms.database.LokiBackupFilesDatabase;
 import org.thoughtcrime.securesms.database.LokiMessageDatabase;
 import org.thoughtcrime.securesms.database.LokiThreadDatabase;
 import org.thoughtcrime.securesms.database.LokiUserDatabase;
+import org.thoughtcrime.securesms.database.MmsDatabase;
+import org.thoughtcrime.securesms.database.PushDatabase;
+import org.thoughtcrime.securesms.database.RecipientDatabase;
+import org.thoughtcrime.securesms.database.SearchDatabase;
 import org.thoughtcrime.securesms.database.SessionContactDatabase;
 import org.thoughtcrime.securesms.database.SessionJobDatabase;
+import org.thoughtcrime.securesms.database.SmsDatabase;
+import org.thoughtcrime.securesms.database.ThreadDatabase;
 
 public class SQLCipherOpenHelper extends SQLiteOpenHelper {
 
@@ -58,9 +58,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
   private static final int lokiV24                          = 45;
   private static final int lokiV25                          = 46;
   private static final int lokiV26                          = 47;
+  private static final int lokiV27                          = 48;
 
   // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes
-  private static final int    DATABASE_VERSION = lokiV26;
+  private static final int    DATABASE_VERSION = lokiV27;
   private static final String DATABASE_NAME    = "signal.db";
 
   private final Context        context;
@@ -296,6 +297,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
         db.execSQL(SessionContactDatabase.getCreateSessionContactTableCommand());
       }
 
+      if (oldVersion < lokiV27) {
+        db.execSQL(RecipientDatabase.getCreateNotificationTypeCommand());
+      }
+
       db.setTransactionSuccessful();
     } finally {
       db.endTransaction();
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt
index 7b454d33c7..b82737932e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationOptionsBottomSheet.kt
@@ -1,16 +1,17 @@
 package org.thoughtcrime.securesms.home
 
 import android.os.Bundle
-import com.google.android.material.bottomsheet.BottomSheetDialogFragment
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
+import androidx.core.view.isVisible
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment
 import kotlinx.android.synthetic.main.fragment_conversation_bottom_sheet.*
 import network.loki.messenger.R
 import org.session.libsession.utilities.recipients.Recipient
 import org.thoughtcrime.securesms.util.UiModeUtilities
 
-public class ConversationOptionsBottomSheet : BottomSheetDialogFragment() {
+public class ConversationOptionsBottomSheet : BottomSheetDialogFragment(), View.OnClickListener {
 
     //FIXME AC: Supplying a recipient directly into the field from an activity
     // is not the best idea. It doesn't survive configuration change.
@@ -22,11 +23,25 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment() {
     var onBlockTapped: (() -> Unit)? = null
     var onUnblockTapped: (() -> Unit)? = null
     var onDeleteTapped: (() -> Unit)? = null
+    var onNotificationTapped: (() -> Unit)? = null
+    var onSetMuteTapped: ((Boolean) -> Unit)? = null
 
     override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
         return inflater.inflate(R.layout.fragment_conversation_bottom_sheet, container, false)
     }
 
+    override fun onClick(v: View?) {
+        when (v) {
+            detailsTextView -> onViewDetailsTapped?.invoke()
+            blockTextView -> onBlockTapped?.invoke()
+            unblockTextView -> onUnblockTapped?.invoke()
+            deleteTextView -> onDeleteTapped?.invoke()
+            notificationsTextView -> onNotificationTapped?.invoke()
+            unMuteNotificationsTextView -> onSetMuteTapped?.invoke(false)
+            muteNotificationsTextView -> onSetMuteTapped?.invoke(true)
+        }
+    }
+
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
         if (!this::recipient.isInitialized) { return dismiss() }
@@ -34,13 +49,19 @@ public class ConversationOptionsBottomSheet : BottomSheetDialogFragment() {
             detailsTextView.visibility = View.VISIBLE
             unblockTextView.visibility = if (recipient.isBlocked) View.VISIBLE else View.GONE
             blockTextView.visibility = if (recipient.isBlocked) View.GONE else View.VISIBLE
-            detailsTextView.setOnClickListener { onViewDetailsTapped?.invoke() }
-            blockTextView.setOnClickListener { onBlockTapped?.invoke() }
-            unblockTextView.setOnClickListener { onUnblockTapped?.invoke() }
+            detailsTextView.setOnClickListener(this)
+            blockTextView.setOnClickListener(this)
+            unblockTextView.setOnClickListener(this)
         } else {
             detailsTextView.visibility = View.GONE
         }
-        deleteTextView.setOnClickListener { onDeleteTapped?.invoke() }
+        unMuteNotificationsTextView.isVisible = recipient.isMuted && !recipient.isLocalNumber
+        muteNotificationsTextView.isVisible = !recipient.isMuted && !recipient.isLocalNumber
+        unMuteNotificationsTextView.setOnClickListener(this)
+        muteNotificationsTextView.setOnClickListener(this)
+        notificationsTextView.isVisible = recipient.isGroupRecipient
+        notificationsTextView.setOnClickListener(this)
+        deleteTextView.setOnClickListener(this)
     }
 
     override fun onStart() {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
index 6c6fd8e1ed..6d42d80de8 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/HomeActivity.kt
@@ -35,8 +35,10 @@ import org.session.libsession.utilities.Util
 import org.session.libsignal.utilities.ThreadUtils
 import org.session.libsignal.utilities.toHexString
 import org.thoughtcrime.securesms.ApplicationContext
+import org.thoughtcrime.securesms.MuteDialog
 import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2
+import org.thoughtcrime.securesms.conversation.v2.utilities.NotificationUtils
 import org.thoughtcrime.securesms.crypto.IdentityKeyUtil
 import org.thoughtcrime.securesms.database.DatabaseFactory
 import org.thoughtcrime.securesms.database.model.ThreadRecord
@@ -44,14 +46,12 @@ import org.thoughtcrime.securesms.dms.CreatePrivateChatActivity
 import org.thoughtcrime.securesms.groups.CreateClosedGroupActivity
 import org.thoughtcrime.securesms.groups.JoinPublicChatActivity
 import org.thoughtcrime.securesms.groups.OpenGroupManager
-import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
-import org.thoughtcrime.securesms.util.*
-import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
 import org.thoughtcrime.securesms.mms.GlideApp
 import org.thoughtcrime.securesms.mms.GlideRequests
 import org.thoughtcrime.securesms.onboarding.SeedActivity
+import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
 import org.thoughtcrime.securesms.preferences.SettingsActivity
-import org.thoughtcrime.securesms.util.IP2Country
+import org.thoughtcrime.securesms.util.*
 import java.io.IOException
 import java.util.*
 
@@ -257,6 +257,16 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
             bottomSheet.dismiss()
             deleteConversation(thread)
         }
+        bottomSheet.onSetMuteTapped = { muted ->
+            bottomSheet.dismiss()
+            setConversationMuted(thread, muted)
+        }
+        bottomSheet.onNotificationTapped = {
+            bottomSheet.dismiss()
+            NotificationUtils.showNotifyDialog(this, thread.recipient) { notifyType ->
+                setNotifyType(thread, notifyType)
+            }
+        }
         bottomSheet.show(supportFragmentManager, bottomSheet.tag)
     }
 
@@ -292,6 +302,35 @@ class HomeActivity : PassphraseRequiredActionBarActivity(), ConversationClickLis
                 }.show()
     }
 
+    private fun setConversationMuted(thread: ThreadRecord, isMuted: Boolean) {
+        if (!isMuted) {
+            ThreadUtils.queue {
+                DatabaseFactory.getRecipientDatabase(this).setMuted(thread.recipient, 0)
+                Util.runOnMain {
+                    recyclerView.adapter!!.notifyDataSetChanged()
+                }
+            }
+        } else {
+            MuteDialog.show(this) { until: Long ->
+                ThreadUtils.queue {
+                    DatabaseFactory.getRecipientDatabase(this).setMuted(thread.recipient, until)
+                    Util.runOnMain {
+                        recyclerView.adapter!!.notifyDataSetChanged()
+                    }
+                }
+            }
+        }
+    }
+
+    private fun setNotifyType(thread: ThreadRecord, newNotifyType: Int) {
+        ThreadUtils.queue {
+            DatabaseFactory.getRecipientDatabase(this).setNotifyType(thread.recipient, newNotifyType)
+            Util.runOnMain {
+                recyclerView.adapter!!.notifyDataSetChanged()
+            }
+        }
+    }
+
     private fun deleteConversation(thread: ThreadRecord) {
         val threadID = thread.threadId
         val recipient = thread.recipient
diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
index 33f25c96b4..6e0fbbd183 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
@@ -41,26 +41,26 @@ import androidx.core.app.NotificationManagerCompat;
 
 import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier;
 import org.session.libsession.utilities.Contact;
-import org.session.libsession.utilities.recipients.Recipient;
 import org.session.libsession.utilities.ServiceUtil;
 import org.session.libsession.utilities.TextSecurePreferences;
-import org.session.libsignal.utilities.Util;
+import org.session.libsession.utilities.recipients.Recipient;
 import org.session.libsignal.utilities.Log;
+import org.session.libsignal.utilities.Util;
 import org.thoughtcrime.securesms.ApplicationContext;
 import org.thoughtcrime.securesms.contactshare.ContactUtil;
-
 import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2;
+import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities;
 import org.thoughtcrime.securesms.database.DatabaseFactory;
 import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
 import org.thoughtcrime.securesms.database.MmsSmsDatabase;
+import org.thoughtcrime.securesms.database.RecipientDatabase;
 import org.thoughtcrime.securesms.database.ThreadDatabase;
 import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
 import org.thoughtcrime.securesms.database.model.MessageRecord;
 import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
-import org.thoughtcrime.securesms.util.SessionMetaProtocol;
-import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities;
 import org.thoughtcrime.securesms.mms.SlideDeck;
 import org.thoughtcrime.securesms.service.KeyCachingService;
+import org.thoughtcrime.securesms.util.SessionMetaProtocol;
 import org.thoughtcrime.securesms.util.SpanUtil;
 
 import java.util.HashSet;
@@ -493,7 +493,16 @@ public class DefaultMessageNotifier implements MessageNotifier {
       }
 
       if (threadRecipients == null || !threadRecipients.isMuted()) {
-        notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck));
+        if (threadRecipients != null && threadRecipients.notifyType == RecipientDatabase.NOTIFY_TYPE_MENTIONS) {
+          // check if mentioned here
+          if (body.toString().contains("@"+TextSecurePreferences.getLocalNumber(context))) {
+            notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck));
+          }
+        } else if (threadRecipients != null && threadRecipients.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) {
+          // do nothing, no notifications
+        } else {
+          notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck));
+        }
       }
     }
 
diff --git a/app/src/main/res/drawable/ic_outline_notification_important_24.xml b/app/src/main/res/drawable/ic_outline_notification_important_24.xml
new file mode 100644
index 0000000000..5a4d7814d4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_notification_important_24.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M10.01,21.01c0,1.1 0.89,1.99 1.99,1.99s1.99,-0.89 1.99,-1.99h-3.98zM12,6c2.76,0 5,2.24 5,5v7L7,18v-7c0,-2.76 2.24,-5 5,-5zM12,1.5c-0.83,0 -1.5,0.67 -1.5,1.5v1.17C7.36,4.85 5,7.65 5,11v6l-2,2v1h18v-1l-2,-2v-6c0,-3.35 -2.36,-6.15 -5.5,-6.83L13.5,3c0,-0.83 -0.67,-1.5 -1.5,-1.5zM11,8h2v4h-2zM11,14h2v2h-2z"/>
+</vector>
diff --git a/app/src/main/res/drawable/ic_outline_notifications_active_24.xml b/app/src/main/res/drawable/ic_outline_notifications_active_24.xml
new file mode 100644
index 0000000000..0a327617e4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_notifications_active_24.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM18,16v-5c0,-3.07 -1.63,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.64,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2zM16,17L8,17v-6c0,-2.48 1.51,-4.5 4,-4.5s4,2.02 4,4.5v6zM7.58,4.08L6.15,2.65C3.75,4.48 2.17,7.3 2.03,10.5h2c0.15,-2.65 1.51,-4.97 3.55,-6.42zM19.97,10.5h2c-0.15,-3.2 -1.73,-6.02 -4.12,-7.85l-1.42,1.43c2.02,1.45 3.39,3.77 3.54,6.42z"/>
+</vector>
diff --git a/app/src/main/res/layout/fragment_conversation_bottom_sheet.xml b/app/src/main/res/layout/fragment_conversation_bottom_sheet.xml
index 4cd4708573..ad1044a539 100644
--- a/app/src/main/res/layout/fragment_conversation_bottom_sheet.xml
+++ b/app/src/main/res/layout/fragment_conversation_bottom_sheet.xml
@@ -32,6 +32,33 @@
         android:visibility="gone"
         tools:visibility="visible" />
 
+    <TextView
+        android:id="@+id/muteNotificationsTextView"
+        style="@style/BottomSheetActionItem"
+        android:drawableStart="@drawable/ic_outline_notifications_off_24"
+        android:text="@string/MuteDialog_mute_notifications"
+        tools:visibility="visible"
+        android:visibility="gone"
+        />
+
+    <TextView
+        android:id="@+id/unMuteNotificationsTextView"
+        style="@style/BottomSheetActionItem"
+        android:drawableStart="@drawable/ic_outline_notifications_active_24"
+        android:text="@string/conversation_muted__unmute"
+        tools:visibility="visible"
+        android:visibility="gone"
+        />
+
+    <TextView
+        android:id="@+id/notificationsTextView"
+        style="@style/BottomSheetActionItem"
+        android:drawableStart="@drawable/ic_outline_notification_important_24"
+        android:text="@string/RecipientPreferenceActivity_notification_settings"
+        tools:visibility="visible"
+        android:visibility="gone"
+        />
+
     <TextView
         android:id="@+id/deleteTextView"
         style="@style/BottomSheetActionItem"
diff --git a/app/src/main/res/menu/menu_conversation_closed_group.xml b/app/src/main/res/menu/menu_conversation_closed_group.xml
index 7ab02e07d9..5e169f5e99 100644
--- a/app/src/main/res/menu/menu_conversation_closed_group.xml
+++ b/app/src/main/res/menu/menu_conversation_closed_group.xml
@@ -13,4 +13,8 @@
         android:title="@string/conversation__menu_leave_group"
         app:showAsAction="collapseActionView"/>
 
+    <item
+        android:title="@string/RecipientPreferenceActivity_notification_settings"
+        android:id="@+id/menu_notification_settings"/>
+
 </menu>
\ No newline at end of file
diff --git a/app/src/main/res/menu/menu_conversation_open_group.xml b/app/src/main/res/menu/menu_conversation_open_group.xml
index 6ff025aadb..3a022a8384 100644
--- a/app/src/main/res/menu/menu_conversation_open_group.xml
+++ b/app/src/main/res/menu/menu_conversation_open_group.xml
@@ -6,4 +6,8 @@
         android:title="@string/ConversationActivity_invite_to_open_group"
         android:id="@+id/menu_invite_to_open_group" />
 
+    <item
+        android:title="@string/RecipientPreferenceActivity_notification_settings"
+        android:id="@+id/menu_notification_settings"/>
+
 </menu>
\ No newline at end of file
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 172aefd74b..1ab8459684 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -187,7 +187,7 @@
       <item>@string/arrays__mute_for_two_hours</item>
       <item>@string/arrays__mute_for_one_day</item>
       <item>@string/arrays__mute_for_seven_days</item>
-      <item>@string/arrays__mute_for_one_year</item>
+      <item>@string/arrays__mute_forever</item>
   </string-array>
 
     <string-array name="pref_notification_privacy_entries">
@@ -244,5 +244,10 @@
         <item>1</item>
         <item>2</item>
     </string-array>
+    <string-array name="notify_types">
+        <item>@string/notify_type_all</item>
+        <item>@string/notify_type_mentions</item>
+        <item>@string/notify_type_none</item>
+    </string-array>
 
 </resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index db2edb6196..ac6ad0b8f4 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -247,6 +247,7 @@
     <string name="RecipientPreferenceActivity_unblock_this_contact_question">Unblock this contact?</string>
     <string name="RecipientPreferenceActivity_you_will_once_again_be_able_to_receive_messages_and_calls_from_this_contact">You will once again be able to receive messages and calls from this contact.</string>
     <string name="RecipientPreferenceActivity_unblock">Unblock</string>
+    <string name="RecipientPreferenceActivity_notification_settings">Notification Settings</string>
 
     <!-- Slide -->
     <string name="Slide_image">Image</string>
@@ -481,6 +482,7 @@
     <string name="arrays__mute_for_one_day">Mute for 1 day</string>
     <string name="arrays__mute_for_seven_days">Mute for 7 days</string>
     <string name="arrays__mute_for_one_year">Mute for 1 year</string>
+    <string name="arrays__mute_forever">Mute forever</string>
 
     <string name="arrays__settings_default">Settings default</string>
     <string name="arrays__enabled">Enabled</string>
@@ -888,4 +890,7 @@
     <string name="dialog_send_seed_title">Warning</string>
     <string name="dialog_send_seed_explanation">This is your recovery phrase. If you send it to someone they\'ll have full access to your account.</string>
     <string name="dialog_send_seed_send_button_title">Send</string>
+    <string name="notify_type_all">All</string>
+    <string name="notify_type_mentions">Mentions</string>
+    <string name="notify_type_none">None</string>
 </resources>
diff --git a/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java b/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java
index 3903908f3f..5b31ded3c5 100644
--- a/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java
+++ b/libsession/src/main/java/org/session/libsession/utilities/recipients/Recipient.java
@@ -26,29 +26,28 @@ import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
 import com.annimon.stream.function.Consumer;
-import com.esotericsoftware.kryo.util.Null;
 
 import org.greenrobot.eventbus.EventBus;
+import org.session.libsession.avatars.ContactColors;
+import org.session.libsession.avatars.ContactPhoto;
+import org.session.libsession.avatars.GroupRecordContactPhoto;
+import org.session.libsession.avatars.ProfileContactPhoto;
+import org.session.libsession.avatars.SystemContactPhoto;
+import org.session.libsession.avatars.TransparentContactPhoto;
 import org.session.libsession.database.StorageProtocol;
 import org.session.libsession.messaging.MessagingModuleConfiguration;
-import org.session.libsession.avatars.TransparentContactPhoto;
 import org.session.libsession.messaging.contacts.Contact;
 import org.session.libsession.utilities.Address;
+import org.session.libsession.utilities.FutureTaskListener;
 import org.session.libsession.utilities.GroupRecord;
-import org.session.libsession.utilities.recipients.RecipientProvider.RecipientDetails;
+import org.session.libsession.utilities.ListenableFutureTask;
+import org.session.libsession.utilities.MaterialColor;
+import org.session.libsession.utilities.ProfilePictureModifiedEvent;
 import org.session.libsession.utilities.TextSecurePreferences;
 import org.session.libsession.utilities.Util;
-import org.session.libsession.utilities.MaterialColor;
+import org.session.libsession.utilities.recipients.RecipientProvider.RecipientDetails;
 import org.session.libsignal.utilities.Log;
 import org.session.libsignal.utilities.guava.Optional;
-import org.session.libsession.avatars.ContactColors;
-import org.session.libsession.avatars.ContactPhoto;
-import org.session.libsession.avatars.GroupRecordContactPhoto;
-import org.session.libsession.avatars.ProfileContactPhoto;
-import org.session.libsession.avatars.SystemContactPhoto;
-import org.session.libsession.utilities.ProfilePictureModifiedEvent;
-import org.session.libsession.utilities.FutureTaskListener;
-import org.session.libsession.utilities.ListenableFutureTask;
 
 import java.util.Collections;
 import java.util.HashSet;
@@ -80,6 +79,7 @@ public class Recipient implements RecipientModifiedListener {
   private @Nullable Uri                  messageRingtone       = null;
   private @Nullable Uri                  callRingtone          = null;
   public            long                 mutedUntil            = 0;
+  public            int                  notifyType            = 0;
   private           boolean              blocked               = false;
   private           VibrateState         messageVibrate        = VibrateState.DEFAULT;
   private           VibrateState         callVibrate           = VibrateState.DEFAULT;
@@ -249,6 +249,7 @@ public class Recipient implements RecipientModifiedListener {
     this.messageRingtone        = details.messageRingtone;
     this.callRingtone           = details.callRingtone;
     this.mutedUntil             = details.mutedUntil;
+    this.notifyType             = details.notifyType;
     this.blocked                = details.blocked;
     this.messageVibrate         = details.messageVibrateState;
     this.callVibrate            = details.callVibrateState;
@@ -547,6 +548,14 @@ public class Recipient implements RecipientModifiedListener {
     notifyListeners();
   }
 
+  public void setNotifyType(int notifyType) {
+    synchronized (this) {
+      this.notifyType = notifyType;
+    }
+
+    notifyListeners();
+  }
+
   public synchronized boolean isBlocked() {
     return blocked;
   }
@@ -769,6 +778,7 @@ public class Recipient implements RecipientModifiedListener {
   public static class RecipientSettings {
     private final boolean                blocked;
     private final long                   muteUntil;
+    private final int                    notifyType;
     private final VibrateState           messageVibrateState;
     private final VibrateState           callVibrateState;
     private final Uri                    messageRingtone;
@@ -790,6 +800,7 @@ public class Recipient implements RecipientModifiedListener {
     private final boolean                forceSmsSelection;
 
     public RecipientSettings(boolean blocked, long muteUntil,
+                      int notifyType,
                       @NonNull VibrateState messageVibrateState,
                       @NonNull VibrateState callVibrateState,
                       @Nullable Uri messageRingtone,
@@ -812,6 +823,7 @@ public class Recipient implements RecipientModifiedListener {
     {
       this.blocked                = blocked;
       this.muteUntil              = muteUntil;
+      this.notifyType             = notifyType;
       this.messageVibrateState    = messageVibrateState;
       this.callVibrateState       = callVibrateState;
       this.messageRingtone        = messageRingtone;
@@ -845,6 +857,10 @@ public class Recipient implements RecipientModifiedListener {
       return muteUntil;
     }
 
+    public int getNotifyType() {
+      return notifyType;
+    }
+
     public @NonNull VibrateState getMessageVibrateState() {
       return messageVibrateState;
     }
diff --git a/libsession/src/main/java/org/session/libsession/utilities/recipients/RecipientProvider.java b/libsession/src/main/java/org/session/libsession/utilities/recipients/RecipientProvider.java
index 8bbabae97a..f87906e4f9 100644
--- a/libsession/src/main/java/org/session/libsession/utilities/recipients/RecipientProvider.java
+++ b/libsession/src/main/java/org/session/libsession/utilities/recipients/RecipientProvider.java
@@ -23,19 +23,20 @@ import android.text.TextUtils;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 
+import org.session.libsession.R;
 import org.session.libsession.messaging.MessagingModuleConfiguration;
-import org.session.libsignal.utilities.guava.Optional;
-import org.session.libsession.utilities.MaterialColor;
 import org.session.libsession.utilities.Address;
 import org.session.libsession.utilities.GroupRecord;
-import org.session.libsession.utilities.recipients.Recipient.RecipientSettings;
-import org.session.libsession.utilities.recipients.Recipient.RegisteredState;
-import org.session.libsession.utilities.recipients.Recipient.UnidentifiedAccessMode;
-import org.session.libsession.utilities.recipients.Recipient.VibrateState;
 import org.session.libsession.utilities.ListenableFutureTask;
+import org.session.libsession.utilities.MaterialColor;
 import org.session.libsession.utilities.SoftHashMap;
 import org.session.libsession.utilities.TextSecurePreferences;
 import org.session.libsession.utilities.Util;
+import org.session.libsession.utilities.recipients.Recipient.RecipientSettings;
+import org.session.libsession.utilities.recipients.Recipient.RegisteredState;
+import org.session.libsession.utilities.recipients.Recipient.UnidentifiedAccessMode;
+import org.session.libsession.utilities.recipients.Recipient.VibrateState;
+import org.session.libsignal.utilities.guava.Optional;
 
 import java.util.HashMap;
 import java.util.LinkedList;
@@ -44,8 +45,6 @@ import java.util.Map;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
 
-import org.session.libsession.R;
-
 class RecipientProvider {
 
   @SuppressWarnings("unused")
@@ -168,6 +167,7 @@ class RecipientProvider {
     @Nullable final Uri                    messageRingtone;
     @Nullable final Uri                    callRingtone;
               final long                   mutedUntil;
+              final int                    notifyType;
     @Nullable final VibrateState           messageVibrateState;
     @Nullable final VibrateState           callVibrateState;
               final boolean                blocked;
@@ -197,6 +197,7 @@ class RecipientProvider {
       this.messageRingtone                 = settings     != null ? settings.getMessageRingtone() : null;
       this.callRingtone                    = settings     != null ? settings.getCallRingtone() : null;
       this.mutedUntil                      = settings     != null ? settings.getMuteUntil() : 0;
+      this.notifyType                      = settings     != null ? settings.getNotifyType() : 0;
       this.messageVibrateState             = settings     != null ? settings.getMessageVibrateState() : null;
       this.callVibrateState                = settings     != null ? settings.getCallVibrateState() : null;
       this.blocked                         = settings     != null && settings.isBlocked();

From 7f047f1c2b3695edc54e997a8e53b1048cb6f3e8 Mon Sep 17 00:00:00 2001
From: Harris <hjubb@users.noreply.github.com>
Date: Tue, 20 Jul 2021 17:34:07 +1000
Subject: [PATCH 2/5] feat: add mention only icon, trying to figure out
 non-repeating push notifications

---
 .../securesms/database/ThreadDatabase.java       | 16 ++++++++--------
 .../securesms/home/ConversationView.kt           | 12 ++++++++++--
 .../notifications/DefaultMessageNotifier.java    | 10 +++++++++-
 3 files changed, 27 insertions(+), 11 deletions(-)

diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
index 16f1f64d4a..9852fdd2e7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
@@ -30,18 +30,18 @@ import com.annimon.stream.Stream;
 
 import net.sqlcipher.database.SQLiteDatabase;
 
-import org.session.libsession.utilities.DistributionTypes;
-import org.session.libsession.utilities.Contact;
 import org.session.libsession.utilities.Address;
-import org.session.libsession.utilities.GroupRecord;
-import org.session.libsession.utilities.recipients.Recipient;
-import org.session.libsession.utilities.recipients.Recipient.RecipientSettings;
+import org.session.libsession.utilities.Contact;
 import org.session.libsession.utilities.DelimiterUtil;
+import org.session.libsession.utilities.DistributionTypes;
+import org.session.libsession.utilities.GroupRecord;
 import org.session.libsession.utilities.TextSecurePreferences;
 import org.session.libsession.utilities.Util;
+import org.session.libsession.utilities.recipients.Recipient;
+import org.session.libsession.utilities.recipients.Recipient.RecipientSettings;
+import org.session.libsignal.utilities.Log;
 import org.session.libsignal.utilities.Pair;
 import org.session.libsignal.utilities.guava.Optional;
-import org.session.libsignal.utilities.Log;
 import org.thoughtcrime.securesms.contactshare.ContactUtil;
 import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
 import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
@@ -49,9 +49,9 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
 import org.thoughtcrime.securesms.database.model.MessageRecord;
 import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
 import org.thoughtcrime.securesms.database.model.ThreadRecord;
-import org.thoughtcrime.securesms.util.SessionMetaProtocol;
 import org.thoughtcrime.securesms.mms.Slide;
 import org.thoughtcrime.securesms.mms.SlideDeck;
+import org.thoughtcrime.securesms.util.SessionMetaProtocol;
 
 import java.io.Closeable;
 import java.util.HashMap;
@@ -651,7 +651,7 @@ public class ThreadDatabase extends Database {
         groupRecord = Optional.absent();
       }
 
-      Recipient          recipient            = Recipient.from(context, address, settings, groupRecord, true);
+      Recipient          recipient            = Recipient.from(context, address, settings, groupRecord, false);
       String             body                 = cursor.getString(cursor.getColumnIndexOrThrow(ThreadDatabase.SNIPPET));
       long               date                 = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.DATE));
       long               count                = cursor.getLong(cursor.getColumnIndexOrThrow(ThreadDatabase.MESSAGE_COUNT));
diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
index 57681963c1..4d404bc5e5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
@@ -14,9 +14,10 @@ import androidx.recyclerview.widget.RecyclerView
 import kotlinx.android.synthetic.main.view_conversation.view.*
 import network.loki.messenger.R
 import org.session.libsession.utilities.recipients.Recipient
-import org.thoughtcrime.securesms.database.model.ThreadRecord
 import org.thoughtcrime.securesms.conversation.v2.utilities.MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded
 import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions
+import org.thoughtcrime.securesms.database.RecipientDatabase
+import org.thoughtcrime.securesms.database.model.ThreadRecord
 import org.thoughtcrime.securesms.mms.GlideRequests
 import org.thoughtcrime.securesms.util.DateUtils
 import java.util.*
@@ -59,7 +60,14 @@ class ConversationView : LinearLayout {
         val senderDisplayName = getUserDisplayName(thread.recipient) ?: thread.recipient.address.toString()
         conversationViewDisplayNameTextView.text = senderDisplayName
         timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
-        muteIndicatorImageView.visibility = if (thread.recipient.isMuted) VISIBLE else GONE
+        val recipient = thread.recipient
+        muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != RecipientDatabase.NOTIFY_TYPE_ALL
+        val drawableRes = if (recipient.isMuted || recipient.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) {
+            R.drawable.ic_outline_notifications_off_24
+        } else {
+            R.drawable.ic_outline_notification_important_24
+        }
+        muteIndicatorImageView.setImageResource(drawableRes)
         val rawSnippet = thread.getDisplayBody(context)
         val snippet = highlightMentions(rawSnippet, thread.threadId, context)
         snippetTextView.text = snippet
diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
index 6e0fbbd183..e06a1c153c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
@@ -238,6 +238,14 @@ public class DefaultMessageNotifier implements MessageNotifier {
       return;
     }
 
+    if (recipients != null && recipients.notifyType != RecipientDatabase.NOTIFY_TYPE_ALL) {
+      // if mentions and no mention then return
+      if (recipients.notifyType == RecipientDatabase.NOTIFY_TYPE_MENTIONS) {
+
+      }
+      return;
+    }
+
     if (isVisible) {
       sendInThreadNotification(context, threads.getRecipientForThreadId(threadId));
     } else if (!homeScreenVisible) {
@@ -280,7 +288,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
         }
 
         sendMultipleThreadNotification(context, notificationState, signal);
-      } else {
+      } else if (notificationState.getMessageCount() > 0){
         sendSingleThreadNotification(context, notificationState, signal, false);
       }
 

From ff853e01b42f218dd02337734a426b18162ed62c Mon Sep 17 00:00:00 2001
From: Harris <hjubb@users.noreply.github.com>
Date: Wed, 21 Jul 2021 13:58:07 +1000
Subject: [PATCH 3/5] fix: notifications deduplicate based on last message ID,
 ConversationActivityV2.kt updates notification by thread ID

---
 .../conversation/v2/ConversationActivityV2.kt | 24 ++++++++---
 .../AbstractNotificationBuilder.java          | 22 +++++++---
 .../notifications/DefaultMessageNotifier.java | 40 ++++++++-----------
 .../SingleRecipientNotificationBuilder.java   | 20 +++++++---
 4 files changed, 66 insertions(+), 40 deletions(-)

diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
index f57726bcb2..b85759f314 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
@@ -24,7 +24,6 @@ import android.widget.Toast
 import androidx.annotation.DimenRes
 import androidx.appcompat.app.AlertDialog
 import androidx.core.view.children
-import androidx.core.view.get
 import androidx.core.view.isVisible
 import androidx.lifecycle.Observer
 import androidx.lifecycle.ViewModelProvider
@@ -53,17 +52,13 @@ import org.session.libsession.messaging.mentions.MentionsManager
 import org.session.libsession.messaging.messages.control.DataExtractionNotification
 import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage
 import org.session.libsession.messaging.messages.signal.OutgoingTextMessage
-import org.session.libsession.messaging.messages.visible.LinkPreview.Companion.from
 import org.session.libsession.messaging.messages.visible.OpenGroupInvitation
-import org.session.libsession.messaging.messages.visible.Quote.Companion.from
 import org.session.libsession.messaging.messages.visible.VisibleMessage
 import org.session.libsession.messaging.open_groups.OpenGroupAPIV2
 import org.session.libsession.messaging.sending_receiving.MessageSender
 import org.session.libsession.messaging.sending_receiving.attachments.Attachment
 import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview
 import org.session.libsession.messaging.sending_receiving.quotes.QuoteModel
-import org.session.libsession.messaging.utilities.UpdateMessageData
-import org.session.libsession.messaging.utilities.UpdateMessageData.Companion.fromJSON
 import org.session.libsession.utilities.Address
 import org.session.libsession.utilities.Address.Companion.fromSerialized
 import org.session.libsession.utilities.MediaTypes
@@ -116,6 +111,23 @@ import org.thoughtcrime.securesms.permissions.Permissions
 import org.thoughtcrime.securesms.util.*
 import java.util.*
 import java.util.concurrent.ExecutionException
+import kotlin.collections.List
+import kotlin.collections.Set
+import kotlin.collections.component1
+import kotlin.collections.component2
+import kotlin.collections.filter
+import kotlin.collections.find
+import kotlin.collections.first
+import kotlin.collections.forEach
+import kotlin.collections.indices
+import kotlin.collections.isNotEmpty
+import kotlin.collections.iterator
+import kotlin.collections.listOf
+import kotlin.collections.mutableListOf
+import kotlin.collections.mutableMapOf
+import kotlin.collections.set
+import kotlin.collections.sortedBy
+import kotlin.collections.toTypedArray
 import kotlin.math.*
 
 // Some things that seemingly belong to the input bar (e.g. the voice message recording UI) are actually
@@ -502,7 +514,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
         } else {
             MarkReadReceiver.process(this, messages)
         }
-        ApplicationContext.getInstance(this).messageNotifier.updateNotification(this)
+        ApplicationContext.getInstance(this).messageNotifier.updateNotification(this, threadID)
     }
 
     override fun inputBarHeightChanged(newValue: Int) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java
index 89296691fa..a359596c91 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/AbstractNotificationBuilder.java
@@ -4,18 +4,21 @@ import android.app.Notification;
 import android.content.Context;
 import android.graphics.Color;
 import android.net.Uri;
+import android.os.Bundle;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.core.app.NotificationCompat;
-import android.text.SpannableStringBuilder;
-import android.text.TextUtils;
 
-import network.loki.messenger.R;
 import org.session.libsession.utilities.NotificationPrivacyPreference;
-import org.session.libsession.utilities.recipients.Recipient;
-import org.session.libsession.utilities.recipients.Recipient.*;
 import org.session.libsession.utilities.TextSecurePreferences;
 import org.session.libsession.utilities.Util;
+import org.session.libsession.utilities.recipients.Recipient;
+import org.session.libsession.utilities.recipients.Recipient.VibrateState;
+
+import network.loki.messenger.R;
 
 public abstract class AbstractNotificationBuilder extends NotificationCompat.Builder {
 
@@ -26,10 +29,11 @@ public abstract class AbstractNotificationBuilder extends NotificationCompat.Bui
 
   protected Context                       context;
   protected NotificationPrivacyPreference privacy;
+  protected final Bundle                  extras;
 
   public AbstractNotificationBuilder(Context context, NotificationPrivacyPreference privacy) {
     super(context);
-
+    extras = new Bundle();
     this.context = context;
     this.privacy = privacy;
 
@@ -97,4 +101,10 @@ public abstract class AbstractNotificationBuilder extends NotificationCompat.Bui
     return text.length() <= MAX_DISPLAY_LENGTH ? text
                                                : text.subSequence(0, MAX_DISPLAY_LENGTH);
   }
+
+  @Override
+  public Notification build() {
+    addExtras(extras);
+    return super.build();
+  }
 }
diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
index e06a1c153c..bdcf916dd4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java
@@ -87,6 +87,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
   private static final String TAG = DefaultMessageNotifier.class.getSimpleName();
 
   public static final  String EXTRA_REMOTE_REPLY = "extra_remote_reply";
+  public static final  String LATEST_MESSAGE_ID_TAG     = "extra_latest_message_id";
 
   private static final  int   FOREGROUND_ID             = 313399;
   private static final  int   SUMMARY_NOTIFICATION_ID   = 1338;
@@ -238,14 +239,6 @@ public class DefaultMessageNotifier implements MessageNotifier {
       return;
     }
 
-    if (recipients != null && recipients.notifyType != RecipientDatabase.NOTIFY_TYPE_ALL) {
-      // if mentions and no mention then return
-      if (recipients.notifyType == RecipientDatabase.NOTIFY_TYPE_MENTIONS) {
-
-      }
-      return;
-    }
-
     if (isVisible) {
       sendInThreadNotification(context, threads.getRecipientForThreadId(threadId));
     } else if (!homeScreenVisible) {
@@ -280,16 +273,9 @@ public class DefaultMessageNotifier implements MessageNotifier {
         lastAudibleNotification = System.currentTimeMillis();
       }
 
-      if (notificationState.hasMultipleThreads()) {
-        if (Build.VERSION.SDK_INT >= 23) {
-          for (long threadId : notificationState.getThreads()) {
-            sendSingleThreadNotification(context, new NotificationState(notificationState.getNotificationsForThread(threadId)), false, true);
-          }
-        }
-
-        sendMultipleThreadNotification(context, notificationState, signal);
-      } else if (notificationState.getMessageCount() > 0){
-        sendSingleThreadNotification(context, notificationState, signal, false);
+      boolean hasMultipleThreads = notificationState.hasMultipleThreads();
+      for (long threadId : notificationState.getThreads()) {
+        sendSingleThreadNotification(context, new NotificationState(notificationState.getNotificationsForThread(threadId)), signal, hasMultipleThreads);
       }
 
       cancelOrphanedNotifications(context, notificationState);
@@ -320,7 +306,20 @@ public class DefaultMessageNotifier implements MessageNotifier {
     List<NotificationItem>             notifications  = notificationState.getNotifications();
     Recipient                          recipient      = notifications.get(0).getRecipient();
     int                                notificationId = (int) (SUMMARY_NOTIFICATION_ID + (bundled ? notifications.get(0).getThreadId() : 0));
+    String                             messageIdTag   = String.valueOf(notifications.get(0).getId());
+
+    NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
+    for (StatusBarNotification notification: notificationManager.getActiveNotifications()) {
+
+      if (messageIdTag.equals(notification.getNotification().extras.getString(LATEST_MESSAGE_ID_TAG))) {
+        return;
+      }
+    }
+
+    long timestamp = notifications.get(0).getTimestamp();
+    if (timestamp != 0) builder.setWhen(timestamp);
 
+    builder.putStringExtra(LATEST_MESSAGE_ID_TAG, messageIdTag);
 
     builder.setThread(notifications.get(0).getRecipient());
     builder.setMessageCount(notificationState.getMessageCount());
@@ -333,11 +332,6 @@ public class DefaultMessageNotifier implements MessageNotifier {
     builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY);
     builder.setAutoCancel(true);
 
-    long timestamp = notifications.get(0).getTimestamp();
-    if (timestamp != 0) builder.setWhen(timestamp);
-
-    long threadID = notifications.get(0).getThreadId();
-
     ReplyMethod replyMethod = ReplyMethod.forRecipient(context, recipient);
 
     boolean canReply = SessionMetaProtocol.canUserReplyToNotification(recipient);
diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java
index 80dd7d2602..7bd4ef4998 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/SingleRecipientNotificationBuilder.java
@@ -15,33 +15,38 @@ import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Build;
 import android.text.SpannableStringBuilder;
+
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.StringRes;
 import androidx.core.app.NotificationCompat;
 import androidx.core.app.NotificationCompat.Action;
 import androidx.core.app.RemoteInput;
+
 import com.bumptech.glide.load.engine.DiskCacheStrategy;
+
 import org.session.libsession.avatars.ContactColors;
 import org.session.libsession.avatars.ContactPhoto;
 import org.session.libsession.avatars.GeneratedContactPhoto;
 import org.session.libsession.messaging.contacts.Contact;
+import org.session.libsession.utilities.NotificationPrivacyPreference;
+import org.session.libsession.utilities.TextSecurePreferences;
+import org.session.libsession.utilities.Util;
+import org.session.libsession.utilities.recipients.Recipient;
 import org.session.libsignal.utilities.Log;
 import org.thoughtcrime.securesms.database.DatabaseFactory;
 import org.thoughtcrime.securesms.database.SessionContactDatabase;
-import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator;
 import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
 import org.thoughtcrime.securesms.mms.GlideApp;
 import org.thoughtcrime.securesms.mms.Slide;
 import org.thoughtcrime.securesms.mms.SlideDeck;
-import org.session.libsession.utilities.NotificationPrivacyPreference;
-import org.session.libsession.utilities.recipients.Recipient;
+import org.thoughtcrime.securesms.util.AvatarPlaceholderGenerator;
 import org.thoughtcrime.securesms.util.BitmapUtil;
-import org.session.libsession.utilities.TextSecurePreferences;
-import org.session.libsession.utilities.Util;
+
 import java.util.LinkedList;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
+
 import network.loki.messenger.R;
 
 public class SingleRecipientNotificationBuilder extends AbstractNotificationBuilder {
@@ -58,6 +63,7 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
   {
     super(context, privacy);
 
+
     setSmallIcon(R.drawable.ic_notification);
     setColor(context.getResources().getColor(R.color.textsecure_primary));
     setCategory(NotificationCompat.CATEGORY_MESSAGE);
@@ -198,6 +204,10 @@ public class SingleRecipientNotificationBuilder extends AbstractNotificationBuil
     return R.string.MessageNotifier_reply;
   }
 
+  public void putStringExtra(String key, String value) {
+    extras.putString(key,value);
+  }
+
   public void addMessageBody(@NonNull Recipient threadRecipient,
                              @NonNull Recipient individualRecipient,
                              @Nullable CharSequence messageBody)

From 7ef9fb2b28ab20d0779c01829c428c90728932fb Mon Sep 17 00:00:00 2001
From: Harris <hjubb@users.noreply.github.com>
Date: Wed, 21 Jul 2021 15:25:48 +1000
Subject: [PATCH 4/5] feat: added notification mentions vector image

---
 .../securesms/home/ConversationView.kt            |  2 +-
 .../res/drawable/ic_notifications_mentions.xml    | 15 +++++++++++++++
 2 files changed, 16 insertions(+), 1 deletion(-)
 create mode 100644 app/src/main/res/drawable/ic_notifications_mentions.xml

diff --git a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
index 4d404bc5e5..9454070e77 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/home/ConversationView.kt
@@ -65,7 +65,7 @@ class ConversationView : LinearLayout {
         val drawableRes = if (recipient.isMuted || recipient.notifyType == RecipientDatabase.NOTIFY_TYPE_NONE) {
             R.drawable.ic_outline_notifications_off_24
         } else {
-            R.drawable.ic_outline_notification_important_24
+            R.drawable.ic_notifications_mentions
         }
         muteIndicatorImageView.setImageResource(drawableRes)
         val rawSnippet = thread.getDisplayBody(context)
diff --git a/app/src/main/res/drawable/ic_notifications_mentions.xml b/app/src/main/res/drawable/ic_notifications_mentions.xml
new file mode 100644
index 0000000000..c078377a5a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_notifications_mentions.xml
@@ -0,0 +1,15 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="4.622348dp"
+    android:height="5.15937dp"
+    android:viewportWidth="4.622348"
+    android:viewportHeight="5.15937"
+    android:tint="?attr/colorControlNormal">
+  <path
+      android:pathData="m2.11667,0.00001c-0.2196,0 -0.39687,0.17727 -0.39687,0.39687v0.18035c-0.75671,0.17992 -1.19062,0.85682 -1.19062,1.67174v1.32291l-0.52917,0.52917v0.26458h4.23333v-0.26458l-0.52917,-0.52917v-0.22427h-0.28939c-0.06727,0 -0.13303,-0.006 -0.19792,-0.015v0.21601,0.32969l-2.15852,-0.0418v-1.5875c0,-0.65617 0.39952,-1.19063 1.05833,-1.19063 0.02183,0 0.04274,0.002 0.06408,0.004 0.11927,-0.18142 0.27784,-0.33414 0.46302,-0.44803 -0.04232,-0.0139 -0.08562,-0.0261 -0.13023,-0.0367v-0.18035c0,-0.2196 -0.17727,-0.39687 -0.39687,-0.39687zM1.5875,4.63021c0,0.29104 0.23812,0.52916 0.52917,0.52916 0.29104,0 0.52917,-0.23812 0.52917,-0.52916z"
+      android:strokeWidth="0.26458332"
+      android:fillColor="#000000"/>
+  <path
+      android:pathData="m3.35531,0.57755c-0.6994,0 -1.26703,0.56762 -1.26703,1.26702 0,0.6994 0.56763,1.26703 1.26703,1.26703h0.63351v-0.2534h-0.63351c-0.54989,0 -1.01362,-0.46374 -1.01362,-1.01363 0,-0.54989 0.46373,-1.01362 1.01362,-1.01362 0.54989,0 1.01362,0.46373 1.01362,1.01362v0.18119c0,0.10009 -0.08996,0.19892 -0.19005,0.19892 -0.1001,0 -0.19005,-0.0988 -0.19005,-0.19892v-0.18119c0,-0.3497 -0.28381,-0.63351 -0.63351,-0.63351 -0.3497,0 -0.63351,0.28381 -0.63351,0.63351 0,0.3497 0.28381,0.63352 0.63351,0.63352 0.17485,0 0.3345,-0.071 0.44853,-0.18625 0.08236,0.11276 0.22426,0.18625 0.37504,0.18625 0.2496,0 0.44346,-0.20273 0.44346,-0.45233v-0.18119c0,-0.6994 -0.56763,-1.26702 -1.26703,-1.26702zM3.35531,2.22468c-0.21033,0 -0.38011,-0.16978 -0.38011,-0.38011 0,-0.21032 0.16978,-0.3801 0.38011,-0.3801 0.21033,0 0.38011,0.16978 0.38011,0.3801 0,0.21033 -0.16978,0.38011 -0.38011,0.38011z"
+      android:strokeWidth="0.12670289"
+      android:fillColor="#000000"/>
+</vector>

From 716dbccb9fdd211c0d0ad94571d805f43804d9e6 Mon Sep 17 00:00:00 2001
From: Harris <hjubb@users.noreply.github.com>
Date: Mon, 26 Jul 2021 09:37:39 +1000
Subject: [PATCH 5/5] feat: add muted forever to ConversationActivityV2.kt

---
 .../conversation/v2/ConversationActivityV2.kt | 20 +++++--------------
 app/src/main/res/values/strings.xml           |  1 +
 2 files changed, 6 insertions(+), 15 deletions(-)

diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
index b85759f314..656da5fd66 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
@@ -111,23 +111,9 @@ import org.thoughtcrime.securesms.permissions.Permissions
 import org.thoughtcrime.securesms.util.*
 import java.util.*
 import java.util.concurrent.ExecutionException
-import kotlin.collections.List
-import kotlin.collections.Set
 import kotlin.collections.component1
 import kotlin.collections.component2
-import kotlin.collections.filter
-import kotlin.collections.find
-import kotlin.collections.first
-import kotlin.collections.forEach
-import kotlin.collections.indices
-import kotlin.collections.isNotEmpty
-import kotlin.collections.iterator
-import kotlin.collections.listOf
-import kotlin.collections.mutableListOf
-import kotlin.collections.mutableMapOf
 import kotlin.collections.set
-import kotlin.collections.sortedBy
-import kotlin.collections.toTypedArray
 import kotlin.math.*
 
 // Some things that seemingly belong to the input bar (e.g. the voice message recording UI) are actually
@@ -732,7 +718,11 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
         muteIconImageView.isVisible = thread.isMuted
         conversationSubtitleView.isVisible = true
         if (thread.isMuted) {
-            conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(thread.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()))
+            if (thread.mutedUntil != Long.MAX_VALUE) {
+                conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_until_date, DateUtils.getFormattedDateTime(thread.mutedUntil, "EEE, MMM d, yyyy HH:mm", Locale.getDefault()))
+            } else {
+                conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_forever)
+            }
         } else if (thread.isGroupRecipient) {
             val openGroup = DatabaseFactory.getLokiThreadDatabase(this).getOpenGroupChat(threadID)
             if (openGroup != null) {
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ac6ad0b8f4..585aec9f28 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -75,6 +75,7 @@
     <string name="ConversationActivity_message">Message</string>
     <string name="ConversationActivity_compose">Compose</string>
     <string name="ConversationActivity_muted_until_date">Muted until %1$s</string>
+    <string name="ConversationActivity_muted_forever">Muted forever</string>
     <string name="ConversationActivity_member_count">%1$d members</string>
     <string name="ConversationActivity_open_group_guidelines">Community Guidelines</string>