Initial message requests feature

Stephen Paul Weber created

Change summary

src/cheogram/res/values/strings.xml                                                          |  1 
src/main/java/eu/siacs/conversations/entities/Conversation.java                              | 25 
src/main/java/eu/siacs/conversations/services/NotificationService.java                       |  8 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java                     |  4 
src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java                           | 53 
src/main/java/eu/siacs/conversations/ui/fragment/settings/NotificationsSettingsFragment.java |  7 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java                | 11 
src/main/res/values/arrays.xml                                                               | 12 
src/main/res/values/defaults.xml                                                             |  1 
src/main/res/xml/preferences_notifications.xml                                               | 15 
10 files changed, 107 insertions(+), 30 deletions(-)

Detailed changes

src/cheogram/res/values/strings.xml 🔗

@@ -48,4 +48,5 @@
     <string name="block_inviter">Block inviter</string>
     <string name="add_bookmark">Add Chat</string>
     <string name="received_invite_from_stranger">Received invite from stranger</string>
+    <string name="pref_chat_requests">Hide chats in Chat Requests area</string>
 </resources>

src/main/java/eu/siacs/conversations/entities/Conversation.java 🔗

@@ -218,6 +218,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
     protected HashMap<String, Thread> threads = new HashMap<>();
     protected Multimap<String, Reaction> reactions = HashMultimap.create();
     private String displayState = null;
+    protected boolean anyMatchSpam = false;
 
     public Conversation(final String name, final Account account, final Jid contactJid,
                         final int mode) {
@@ -1388,19 +1389,36 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
         }
     }
 
+    public void checkSpam(Message... messages) {
+        if (anyMatchSpam) return;
+
+        final var locale = java.util.Locale.getDefault();
+        final var script = locale.getScript();
+        for (final var m : messages) {
+            final var body = m.getRawBody();
+            if (body.length() > 320 || (!"Cyrl".equals(script) && body.matches(".*\\p{IsCyrillic}.*")) || body.matches(".*(?:\\n.*\\n.*\\n|[Aa]\\s*d\\s*v\\s*v\\s*e\\s*r\\s*t|[Pp]romotion|[Dd][Dd][Oo][Ss]|[Ee]scrow|payout|seller|\\?OTR|write me when will be|[Pp]rii?vee?t|there online|bit\\.ly|goo\\.gl|tinyurl\\.com|tiny\\.cc|lc\\.chat|is\\.gd|soo\\.gd|s2r\\.co|clicky\\.me|budrul\\.com|bc\\.vc|uguu\\.se).*")) {
+                anyMatchSpam = true;
+                return;
+            }
+        }
+    }
+
     public void add(Message message) {
+        checkSpam(message);
         synchronized (this.messages) {
             this.messages.add(message);
         }
     }
 
     public void prepend(int offset, Message message) {
+        checkSpam(message);
         synchronized (this.messages) {
             this.messages.add(Math.min(offset, this.messages.size()), message);
         }
     }
 
     public void addAll(int index, List<Message> messages) {
+        checkSpam(messages.toArray(new Message[0]));
         synchronized (this.messages) {
             this.messages.addAll(index, messages);
         }
@@ -1494,6 +1512,13 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
         return sentMessagesCount() > 0;
     }
 
+    public boolean isChatRequest(final String pref) {
+        if ("disable".equals(pref)) return false;
+        if ("strangers".equals(pref)) return isWithStranger();
+        if (!isWithStranger() && !strangerInvited()) return false;
+        return anyMatchSpam;
+    }
+
     public boolean isWithStranger() {
         final Contact contact = getContact();
         return mode == MODE_SINGLE

src/main/java/eu/siacs/conversations/services/NotificationService.java 🔗

@@ -389,10 +389,11 @@ public class NotificationService {
 
     private boolean notifyMessage(final Message message) {
         final Conversation conversation = (Conversation) message.getConversation();
+        final var chatRequestsPref = mXmppConnectionService.getStringPreference("chat_requests", R.string.default_chat_requests);
         return message.getStatus() == Message.STATUS_RECEIVED
                 && !conversation.isMuted()
                 && (conversation.alwaysNotify() || (wasHighlightedOrPrivate(message) || (conversation.notifyReplies() && wasReplyToMe(message))))
-                && (!conversation.isWithStranger() || notificationsFromStrangers())
+                && !conversation.isChatRequest(chatRequestsPref)
                 && message.getType() != Message.TYPE_RTP_SESSION;
     }
 
@@ -401,11 +402,6 @@ public class NotificationService {
                 && message.getStatus() == Message.STATUS_RECEIVED;
     }
 
-    public boolean notificationsFromStrangers() {
-        return mXmppConnectionService.getBooleanPreference(
-                "notifications_from_strangers", R.bool.notifications_from_strangers);
-    }
-
     private boolean isQuietHours(Account account) {
         return isQuietHours(mXmppConnectionService, account);
     }

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -4985,6 +4985,10 @@ public class XmppConnectionService extends Service {
         return getPreferences().getBoolean(name, getResources().getBoolean(res));
     }
 
+    public String getStringPreference(String name, @BoolRes int res) {
+        return getPreferences().getString(name, getResources().getString(res));
+    }
+
     public boolean confirmMessages() {
         return getBooleanPreference("confirm_messages", R.bool.confirm_messages);
     }

src/main/java/eu/siacs/conversations/ui/ConversationsActivity.java 🔗

@@ -152,13 +152,14 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
     public static final long DRAWER_MANAGE_ACCOUNT = 4;
     public static final long DRAWER_MANAGE_PHONE_ACCOUNTS = 5;
     public static final long DRAWER_CHANNELS = 6;
-    public static final long DRAWER_SETTINGS = 7;
-    public static final long DRAWER_START_CHAT = 8;
-    public static final long DRAWER_START_CHAT_CONTACT = 9;
-    public static final long DRAWER_START_CHAT_NEW = 10;
-    public static final long DRAWER_START_CHAT_GROUP = 11;
-    public static final long DRAWER_START_CHAT_PUBLIC = 12;
-    public static final long DRAWER_START_CHAT_DISCOVER = 13;
+    public static final long DRAWER_CHAT_REQUESTS = 7;
+    public static final long DRAWER_SETTINGS = 8;
+    public static final long DRAWER_START_CHAT = 9;
+    public static final long DRAWER_START_CHAT_CONTACT = 10;
+    public static final long DRAWER_START_CHAT_NEW = 11;
+    public static final long DRAWER_START_CHAT_GROUP = 12;
+    public static final long DRAWER_START_CHAT_PUBLIC = 13;
+    public static final long DRAWER_START_CHAT_DISCOVER = 14;
 
     //secondary fragment (when holding the conversation, must be initialized before refreshing the overview fragment
     private static final @IdRes
@@ -200,6 +201,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
 
         if (accountHeader == null) return;
 
+        final var chatRequestsPref = xmppConnectionService.getStringPreference("chat_requests", R.string.default_chat_requests);
         final var accountUnreads = new HashMap<Account, Integer>();
         binding.drawer.apply(dr -> {
             final var items = binding.drawer.getItemAdapter().getAdapterItems();
@@ -208,6 +210,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
             var totalUnread = 0;
             var dmUnread = 0;
             var channelUnread = 0;
+            var chatRequests = 0;
             final var selectedAccount = selectedAccount();
             populateWithOrderedConversations(conversations, false, false);
             for (final var c : conversations) {
@@ -219,6 +222,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
                     } else {
                         dmUnread += unread;
                     }
+                    if (c.isChatRequest(chatRequestsPref)) chatRequests++;
                 }
                 var accountUnread = accountUnreads.get(c.getAccount());
                 if (accountUnread == null) accountUnread = 0;
@@ -255,6 +259,27 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
                 new com.mikepenz.materialdrawer.holder.StringHolder(channelUnread > 0 ? new Integer(channelUnread).toString() : null)
             );
 
+            if (chatRequests > 0) {
+                if (binding.drawer.getItemAdapter().getAdapterPosition(DRAWER_CHAT_REQUESTS) < 0) {
+                    final var color = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorPrimaryContainer);
+                    final var textColor = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorOnPrimaryContainer);
+                    final var requests = new com.mikepenz.materialdrawer.model.PrimaryDrawerItem();
+                    requests.setIdentifier(DRAWER_CHAT_REQUESTS);
+                    com.mikepenz.materialdrawer.model.interfaces.NameableKt.setNameText(requests, "Chat Requests");
+                    com.mikepenz.materialdrawer.model.interfaces.IconableKt.setIconRes(requests, R.drawable.ic_person_add_24dp);
+                    requests.setBadgeStyle(new com.mikepenz.materialdrawer.holder.BadgeStyle(com.mikepenz.materialdrawer.R.drawable.material_drawer_badge, color, color, textColor));
+                    binding.drawer.getItemAdapter().add(binding.drawer.getItemAdapter().getGlobalPosition(binding.drawer.getItemAdapter().getAdapterPosition(DRAWER_CHANNELS) + 1), requests);
+                }
+                com.mikepenz.materialdrawer.util.MaterialDrawerSliderViewExtensionsKt.updateBadge(
+                    binding.drawer,
+                    DRAWER_CHAT_REQUESTS,
+                    new com.mikepenz.materialdrawer.holder.StringHolder(chatRequests > 0 ? new Integer(chatRequests).toString() : null)
+                );
+            } else {
+                binding.drawer.getItemAdapter().removeByIdentifier(DRAWER_CHAT_REQUESTS);
+            }
+
+            final var endOfMainFilters = chatRequests > 0 ? 6 : 5;
             long id = 1000;
             final var inDrawer = new HashMap<Tag, Long>();
             for (final var item : ImmutableList.copyOf(items)) {
@@ -283,11 +308,11 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
                     final var color = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorPrimaryContainer);
                     final var textColor = MaterialColors.getColor(binding.drawer, com.google.android.material.R.attr.colorOnPrimaryContainer);
                     item.setBadgeStyle(new com.mikepenz.materialdrawer.holder.BadgeStyle(com.mikepenz.materialdrawer.R.drawable.material_drawer_badge, color, color, textColor));
-                    binding.drawer.getItemAdapter().add(binding.drawer.getItemAdapter().getGlobalPosition(5), item);
+                    binding.drawer.getItemAdapter().add(binding.drawer.getItemAdapter().getGlobalPosition(endOfMainFilters), item);
                 }
             }
 
-            items.subList(5, 5 + tags.size()).sort((x, y) -> x.getTag() == null ? -1 : ((Comparable) x.getTag()).compareTo(y.getTag()));
+            items.subList(endOfMainFilters, endOfMainFilters + tags.size()).sort((x, y) -> x.getTag() == null ? -1 : ((Comparable) x.getTag()).compareTo(y.getTag()));
             binding.drawer.getItemAdapter().getFastAdapter().notifyDataSetChanged();
             return kotlin.Unit.INSTANCE;
         });
@@ -489,7 +514,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
                 launchStartConversation(R.id.create_public_channel);
             } else if (id == DRAWER_START_CHAT_DISCOVER) {
                 launchStartConversation(R.id.discover_public_channels);
-            } else if (id == DRAWER_ALL_CHATS || id == DRAWER_UNREAD_CHATS || id == DRAWER_DIRECT_MESSAGES || id == DRAWER_CHANNELS) {
+            } else if (id == DRAWER_ALL_CHATS || id == DRAWER_UNREAD_CHATS || id == DRAWER_DIRECT_MESSAGES || id == DRAWER_CHANNELS || id == DRAWER_CHAT_REQUESTS) {
                 selectedTag.clear();
                 mainFilter = id;
                 binding.drawer.getSelectExtension().deselect();
@@ -516,7 +541,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
 
         binding.drawer.setOnDrawerItemLongClickListener((v, drawerItem, pos) -> {
             final var id = drawerItem.getIdentifier();
-            if (id == DRAWER_ALL_CHATS || id == DRAWER_UNREAD_CHATS || id == DRAWER_DIRECT_MESSAGES || id == DRAWER_CHANNELS) {
+            if (id == DRAWER_ALL_CHATS || id == DRAWER_UNREAD_CHATS || id == DRAWER_DIRECT_MESSAGES || id == DRAWER_CHANNELS || id == DRAWER_CHAT_REQUESTS) {
                 selectedTag.clear();
                 mainFilter = id;
                 binding.drawer.getSelectExtension().deselect();
@@ -639,6 +664,7 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
     }
 
     protected void filterByMainFilter(List<Conversation> list) {
+         final var chatRequests = xmppConnectionService.getStringPreference("chat_requests", R.string.default_chat_requests);
          for (final var c : ImmutableList.copyOf(list)) {
             if (mainFilter == DRAWER_CHANNELS && c.getMode() != Conversation.MODE_MULTI) {
                 list.remove(c);
@@ -646,6 +672,11 @@ public class ConversationsActivity extends XmppActivity implements OnConversatio
                 list.remove(c);
             } else if (mainFilter == DRAWER_UNREAD_CHATS && c.unreadCount(xmppConnectionService) < 1) {
                 list.remove(c);
+            } else if (mainFilter == DRAWER_CHAT_REQUESTS && !c.isChatRequest(chatRequests)) {
+                list.remove(c);
+            }
+            if (mainFilter != DRAWER_CHAT_REQUESTS && c.isChatRequest(chatRequests)) {
+                list.remove(c);
             }
         }
     }

src/main/java/eu/siacs/conversations/ui/fragment/settings/NotificationsSettingsFragment.java 🔗

@@ -15,6 +15,7 @@ import androidx.activity.result.ActivityResultLauncher;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.preference.Preference;
+import androidx.preference.ListPreference;
 
 import com.google.common.base.Optional;
 
@@ -65,6 +66,7 @@ public class NotificationsSettingsFragment extends XmppPreferenceFragment {
         final var notificationHeadsUp = findPreference(AppSettings.NOTIFICATION_HEADS_UP);
         final var notificationVibrate = findPreference(AppSettings.NOTIFICATION_VIBRATE);
         final var notificationLed = findPreference(AppSettings.NOTIFICATION_LED);
+        final var chatRequests = (ListPreference) findPreference("chat_requests");
         final var foregroundService = findPreference(AppSettings.KEEP_FOREGROUND_SERVICE);
         if (messageNotificationSettings == null
                 || fullscreenNotification == null
@@ -91,6 +93,11 @@ public class NotificationsSettingsFragment extends XmppPreferenceFragment {
                         .canUseFullScreenIntent()) {
             fullscreenNotification.setVisible(false);
         }
+
+        final var sharedPreferences = getPreferenceManager().getSharedPreferences();
+        if (!sharedPreferences.getBoolean("notifications_from_strangers", true) && sharedPreferences.getString("chat_requests", null) == null) {
+            chatRequests.setValue("strangers");
+        }
     }
 
     @Override

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java 🔗

@@ -16,6 +16,7 @@ import com.google.common.collect.Collections2;
 import com.google.common.collect.ComparisonChain;
 import com.google.common.collect.ImmutableSet;
 
+import eu.siacs.conversations.R;
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Contact;
@@ -255,13 +256,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
     }
 
     private boolean isWithStrangerAndStrangerNotificationsAreOff(final Account account, Jid with) {
-        final boolean notifyForStrangers =
-                mXmppConnectionService.getNotificationService().notificationsFromStrangers();
-        if (notifyForStrangers) {
-            return false;
-        }
-        final Contact contact = account.getRoster().getContact(with);
-        return !contact.showInContactList();
+        final var chatRequestsPref = mXmppConnectionService.getStringPreference("chat_requests", R.string.default_chat_requests);
+        final var conversation = mXmppConnectionService.findOrCreateConversation(account, with, false, true);
+        return conversation.isChatRequest(chatRequestsPref);
     }
 
     ScheduledFuture<?> schedule(

src/main/res/values/arrays.xml 🔗

@@ -106,6 +106,18 @@
 		<item>@string/video_original</item>
 	</string-array>
 
+	<string-array name="chat_requests_entries">
+		<item>Never</item>
+		<item>Suspected SPAM</item>
+		<item>Chats from strangers</item>
+	</string-array>
+
+	<string-array name="chat_requests_values">
+		<item>disable</item>
+		<item>spam</item>
+		<item>strangers</item>
+	</string-array>
+
 	<string-array name="channel_discovery_entries">
 		<item>@string/jabber_network</item>
 		<item>@string/local_server</item>

src/main/res/values/defaults.xml 🔗

@@ -55,4 +55,5 @@
     <bool name="show_link_previews">true</bool>
     <bool name="compose_rich_text">true</bool>
     <bool name="auto_accept_unmetered">true</bool>
+    <string name="default_chat_requests">spam</string>
 </resources>

src/main/res/xml/preferences_notifications.xml 🔗

@@ -1,5 +1,6 @@
 <?xml version="1.0" encoding="utf-8"?>
-<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
     <!-- This preference links to the OS notification settings and only shows up on API >= 26 (Android 8) -->
     <PreferenceScreen
         android:icon="@drawable/ic_chat_24dp"
@@ -65,12 +66,14 @@
         android:key="grace_period_length"
         android:summary="@string/pref_notification_grace_period_summary"
         android:title="@string/pref_notification_grace_period" />
-    <SwitchPreferenceCompat
-        android:defaultValue="@bool/notifications_from_strangers"
+    <ListPreference
+        android:defaultValue="@string/default_chat_requests"
+        android:entries="@array/chat_requests_entries"
+        android:entryValues="@array/chat_requests_values"
         android:icon="@drawable/ic_domino_mask_24dp"
-        android:key="notifications_from_strangers"
-        android:summary="@string/pref_notifications_from_strangers_summary"
-        android:title="@string/pref_notifications_from_strangers" />
+        android:key="chat_requests"
+        android:title="@string/pref_chat_requests"
+        app:useSimpleSummaryProvider="true" />
 
     <SwitchPreferenceCompat
         android:defaultValue="@bool/enable_foreground_service"