From cb0442b84a0b40bb58b0741c420970ba07c1a889 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Wed, 21 Sep 2022 20:01:18 -0500 Subject: [PATCH] Tag navigation UI Show horizontal list of tags available in current search results below the search box, acts sort of like an autocomplete or you can just tap on it, making tag naviation easier so you don't have to find something that matches that tag or type it out to use it. --- .../conversations/entities/Bookmark.java | 18 +++- .../siacs/conversations/entities/Contact.java | 8 +- .../conversations/entities/ListItem.java | 15 ++++ .../ui/StartConversationActivity.java | 87 +++++++++++++++++++ src/main/res/layout/actionview_search.xml | 8 +- 5 files changed, 127 insertions(+), 9 deletions(-) diff --git a/src/main/java/eu/siacs/conversations/entities/Bookmark.java b/src/main/java/eu/siacs/conversations/entities/Bookmark.java index 6ab62f6ea38da3345cd5888d0cd265e79aa02d5d..c9e39bafb4ac9ae134bb605bf0b23e72e812db77 100644 --- a/src/main/java/eu/siacs/conversations/entities/Bookmark.java +++ b/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) { diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index cb66538c55ea8ffb13968334997d26cfe23cc8f7..bcac60f933f9545b53d7e5646bfad0d8a214c862 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/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]); } } diff --git a/src/main/java/eu/siacs/conversations/entities/ListItem.java b/src/main/java/eu/siacs/conversations/entities/ListItem.java index adc7666ce0be2fa545b5438afc2fb28bd8fcc445..ee27dff5da4005b903951af7832a88bfc6361a80 100644 --- a/src/main/java/eu/siacs/conversations/entities/ListItem.java +++ b/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, 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); diff --git a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java b/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java index 830ff888460072126b9934c65f5cbb6b2daeb3ac..75381aadac18b3c3f4cfb9012b7c4607651f2be3 100644 --- a/src/main/java/eu/siacs/conversations/ui/StartConversationActivity.java +++ b/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 contacts = new ArrayList<>(); private ListItemAdapter mContactsAdapter; + private TagsAdapter mTagsAdapter = new TagsAdapter(); private final List conferences = new ArrayList<>(); private ListItemAdapter mConferenceAdapter; private final List 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 tags = new ArrayList<>(); final List 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> 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 { + 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 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 tags) { + ListItem.Tag channelTag = new ListItem.Tag("Channel", UIHelper.getColorForName("Channel", true)); + String needle = mSearchEditText == null ? "" : mSearchEditText.getText().toString().toLowerCase(Locale.US).trim(); + HashSet 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(); + } + } } diff --git a/src/main/res/layout/actionview_search.xml b/src/main/res/layout/actionview_search.xml index 90783b776285ad1cafc52e1990fb41ad5294c67d..5ef204372af8d1f9704fdb1a147094a1760d5a23 100644 --- a/src/main/res/layout/actionview_search.xml +++ b/src/main/res/layout/actionview_search.xml @@ -15,4 +15,10 @@ android:imeOptions="flagNoExtractUi|actionSearch" android:inputType="textEmailAddress|textNoSuggestions"/> - \ No newline at end of file + + +