Merge commit 'cb0442b84a0b40bb58b0741c420970ba07c1a889'

Stephen Paul Weber created

* commit 'cb0442b84a0b40bb58b0741c420970ba07c1a889':
  Tag navigation UI

Change summary

src/main/java/eu/siacs/conversations/entities/Bookmark.java            | 18 
src/main/java/eu/siacs/conversations/entities/Contact.java             |  8 
src/main/java/eu/siacs/conversations/entities/ListItem.java            | 15 
src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java | 87 
src/main/res/layout/actionview_search.xml                              |  8 
5 files changed, 127 insertions(+), 9 deletions(-)

Detailed changes

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

@@ -196,10 +196,20 @@ public class Bookmark extends Element implements ListItem {
 			return true;
 		}
 		needle = needle.toLowerCase(Locale.US);
-		final Jid jid = getJid();
-		return (jid != null && jid.toString().contains(needle)) ||
-			getDisplayName().toLowerCase(Locale.US).contains(needle) ||
-			matchInTag(context, needle);
+		String[] parts = needle.split("[,\\s]+");
+		if (parts.length > 1) {
+			for (String part : parts) {
+				if (!match(context, part)) {
+					return false;
+				}
+			}
+			return true;
+		} else {
+			final Jid jid = getJid();
+			return (jid != null && jid.toString().contains(parts[0])) ||
+				getDisplayName().toLowerCase(Locale.US).contains(parts[0]) ||
+				matchInTag(context, parts[0]);
+		}
 	}
 
 	private boolean matchInTag(Context context, String needle) {

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

@@ -216,7 +216,7 @@ public class Contact implements ListItem, Blockable {
             return true;
         }
         needle = needle.toLowerCase(Locale.US).trim();
-        String[] parts = needle.split("\\s+");
+        String[] parts = needle.split("[,\\s]+");
         if (parts.length > 1) {
             for (String part : parts) {
                 if (!match(context, part)) {
@@ -225,9 +225,9 @@ public class Contact implements ListItem, Blockable {
             }
             return true;
         } else {
-            return jid.toString().contains(needle) ||
-                    getDisplayName().toLowerCase(Locale.US).contains(needle) ||
-                    matchInTag(context, needle);
+            return jid.toString().contains(parts[0]) ||
+                    getDisplayName().toLowerCase(Locale.US).contains(parts[0]) ||
+                    matchInTag(context, parts[0]);
         }
     }
 

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

@@ -3,6 +3,7 @@ package eu.siacs.conversations.entities;
 import android.content.Context;
 
 import java.util.List;
+import java.util.Locale;
 
 import eu.siacs.conversations.services.AvatarService;
 import eu.siacs.conversations.xmpp.Jid;
@@ -31,6 +32,20 @@ public interface ListItem extends Comparable<ListItem>, AvatarService.Avatarable
 		public String getName() {
 			return this.name;
 		}
+
+		public String toString() {
+			return getName();
+		}
+
+		public boolean equals(Object o) {
+			if (!(o instanceof Tag)) return false;
+			Tag ot = (Tag) o;
+			return name.toLowerCase(Locale.US).equals(ot.getName().toLowerCase(Locale.US)) && color == ot.getColor();
+		}
+
+		public int hashCode() {
+			return name.toLowerCase(Locale.US).hashCode();
+		}
 	}
 
 	boolean match(Context context, final String needle);

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

@@ -21,6 +21,7 @@ import android.util.Pair;
 import android.view.ContextMenu;
 import android.view.ContextMenu.ContextMenuInfo;
 import android.view.KeyEvent;
+import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
@@ -49,6 +50,8 @@ import androidx.databinding.DataBindingUtil;
 import androidx.fragment.app.Fragment;
 import androidx.fragment.app.FragmentManager;
 import androidx.fragment.app.FragmentTransaction;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.LinearLayoutManager;
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
 import androidx.viewpager.widget.PagerAdapter;
 import androidx.viewpager.widget.ViewPager;
@@ -57,10 +60,16 @@ import com.google.android.material.textfield.TextInputLayout;
 import com.leinardi.android.speeddial.SpeedDialActionItem;
 import com.leinardi.android.speeddial.SpeedDialView;
 
+import java.util.Arrays;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
+import java.util.Map;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.stream.Collectors;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -83,6 +92,7 @@ import eu.siacs.conversations.ui.util.PendingItem;
 import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
 import eu.siacs.conversations.ui.widget.SwipeRefreshListFragment;
 import eu.siacs.conversations.utils.AccountUtils;
+import eu.siacs.conversations.utils.UIHelper;
 import eu.siacs.conversations.utils.XmppUri;
 import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
@@ -101,6 +111,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
     private ListPagerAdapter mListPagerAdapter;
     private final List<ListItem> contacts = new ArrayList<>();
     private ListItemAdapter mContactsAdapter;
+    private TagsAdapter mTagsAdapter = new TagsAdapter();
     private final List<ListItem> conferences = new ArrayList<>();
     private ListItemAdapter mConferenceAdapter;
     private final List<String> mActivatedAccounts = new ArrayList<>();
@@ -668,6 +679,9 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
         mSearchEditText = mSearchView.findViewById(R.id.search_field);
         mSearchEditText.addTextChangedListener(mSearchTextWatcher);
         mSearchEditText.setOnEditorActionListener(mSearchDone);
+        RecyclerView tags = mSearchView.findViewById(R.id.tags);
+        tags.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
+        tags.setAdapter(mTagsAdapter);
         String initialSearchValue = mInitialSearchValue.pop();
         if (initialSearchValue != null) {
             mMenuSearchView.expandActionView();
@@ -959,6 +973,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
 
     protected void filterContacts(String needle) {
         this.contacts.clear();
+        ArrayList<ListItem.Tag> tags = new ArrayList<>();
         final List<Account> accounts = xmppConnectionService.getAccounts();
         boolean foundSopranica = false;
         for (Account account : accounts) {
@@ -970,6 +985,7 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
                             || (needle != null && !needle.trim().isEmpty())
                             || s.compareTo(Presence.Status.OFFLINE) < 0)) {
                         this.contacts.add(contact);
+                        tags.addAll(contact.getTags(this));
                     }
                 }
 
@@ -979,11 +995,22 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
                             foundSopranica = true;
                         }
                         this.contacts.add(bookmark);
+                        tags.addAll(bookmark.getTags(this));
                     }
                 }
             }
         }
 
+        Comparator<Map.Entry<ListItem.Tag,Integer>> sortTagsBy = Map.Entry.comparingByValue(Comparator.reverseOrder());
+        sortTagsBy = sortTagsBy.thenComparing(entry -> entry.getKey().getName());
+
+        mTagsAdapter.setTags(
+            tags.stream()
+            .collect(Collectors.toMap((x) -> x, (t) -> 1, (c1, c2) -> c1 + c2))
+            .entrySet().stream()
+            .sorted(sortTagsBy)
+            .map(e -> e.getKey()).collect(Collectors.toList())
+        );
         Collections.sort(this.contacts);
 
         final boolean sopranicaDeleted = getPreferences().getBoolean("cheogram_sopranica_bookmark_deleted", false);
@@ -1385,4 +1412,64 @@ public class StartConversationActivity extends XmppActivity implements XmppConne
             return false;
         }
     }
+
+    class TagsAdapter extends RecyclerView.Adapter<TagsAdapter.ViewHolder> {
+        class ViewHolder extends RecyclerView.ViewHolder {
+            protected TextView tv;
+
+            public ViewHolder(View v) {
+                super(v);
+                tv = (TextView) v;
+                tv.setOnClickListener(view -> {
+                    String needle = mSearchEditText.getText().toString();
+                    String tag = tv.getText().toString();
+                    String[] parts = needle.split("[,\\s]+");
+                    if(needle.isEmpty()) {
+                        needle = tag;
+                    } else if (tag.toLowerCase(Locale.US).contains(parts[parts.length-1])) {
+                        needle = needle.replace(parts[parts.length-1], tag);
+                    } else {
+                        needle += ", " + tag;
+                    }
+                    mSearchEditText.setText("");
+                    mSearchEditText.append(needle);
+                    filter(needle);
+                });
+            }
+
+            public void setTag(ListItem.Tag tag) {
+                tv.setText(tag.getName());
+                tv.setBackgroundColor(tag.getColor());
+            }
+        }
+
+        protected List<ListItem.Tag> tags = new ArrayList<>();
+
+        @Override
+        public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int i) {
+            View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.list_item_tag, null);
+            return new ViewHolder(view);
+        }
+
+        @Override
+        public void onBindViewHolder(ViewHolder viewHolder, int i) {
+            viewHolder.setTag(tags.get(i));
+        }
+
+        @Override
+        public int getItemCount() {
+            return tags.size();
+        }
+
+        public void setTags(final List<ListItem.Tag> tags) {
+            ListItem.Tag channelTag = new ListItem.Tag("Channel", UIHelper.getColorForName("Channel", true));
+            String needle = mSearchEditText == null ? "" : mSearchEditText.getText().toString().toLowerCase(Locale.US).trim();
+            HashSet<String> parts = new HashSet<>(Arrays.asList(needle.split("[,\\s]+")));
+            this.tags = tags.stream().filter(
+                tag -> !tag.equals(channelTag) && !parts.contains(tag.getName().toLowerCase(Locale.US))
+            ).collect(Collectors.toList());
+            if (!parts.contains("channel") && tags.contains(channelTag)) this.tags.add(0, channelTag);
+            notifyDataSetChanged();
+        }
+    }
 }

src/main/res/layout/actionview_search.xml 🔗

@@ -15,4 +15,10 @@
         android:imeOptions="flagNoExtractUi|actionSearch"
         android:inputType="textEmailAddress|textNoSuggestions"/>
 
-</RelativeLayout>
+    <androidx.recyclerview.widget.RecyclerView
+        android:id="@+id/tags"
+        android:layout_below="@+id/search_field"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content" />
+
+</RelativeLayout>