From f3c1f238d03f94cafc30e176dfe13969a444318f Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Mon, 9 Sep 2024 11:30:44 -0500 Subject: [PATCH] Port EmojiSearch to use Autocomplete --- .../com/cheogram/android/EmojiSearch.java | 67 ++++++++++---- src/cheogram/res/layout/emoji_search.xml | 16 ---- .../ui/ConversationFragment.java | 90 ++++++++----------- 3 files changed, 86 insertions(+), 87 deletions(-) delete mode 100644 src/cheogram/res/layout/emoji_search.xml diff --git a/src/cheogram/java/com/cheogram/android/EmojiSearch.java b/src/cheogram/java/com/cheogram/android/EmojiSearch.java index eebf5d197ab300d9f58d0299190e535585c7d3c5..72c6ef3ab50ba7319ab8c952db455518e4fa834f 100644 --- a/src/cheogram/java/com/cheogram/android/EmojiSearch.java +++ b/src/cheogram/java/com/cheogram/android/EmojiSearch.java @@ -8,9 +8,11 @@ import android.text.SpannableStringBuilder; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; import androidx.databinding.DataBindingUtil; +import androidx.recyclerview.widget.DiffUtil; +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.RecyclerView; import com.google.common.collect.Lists; import com.google.common.io.CharStreams; @@ -25,6 +27,7 @@ import java.util.List; import java.util.PriorityQueue; import java.util.Set; import java.util.TreeSet; +import java.util.function.Consumer; import me.xdrop.fuzzywuzzy.FuzzySearch; import me.xdrop.fuzzywuzzy.model.BoundExtractedResult; @@ -87,8 +90,8 @@ public class EmojiSearch { return result; } - public EmojiSearchAdapter makeAdapter(Activity context) { - return new EmojiSearchAdapter(context); + public EmojiSearchAdapter makeAdapter(Consumer callback) { + return new EmojiSearchAdapter(callback); } public static class ResultPQ extends PriorityQueue> { @@ -198,38 +201,68 @@ public class EmojiSearch { } } - public class EmojiSearchAdapter extends ArrayAdapter { + public class EmojiSearchAdapter extends ListAdapter { ReplacingSerialSingleThreadExecutor executor = new ReplacingSerialSingleThreadExecutor("EmojiSearchAdapter"); - public EmojiSearchAdapter(Activity context) { - super(context, 0); + static final DiffUtil.ItemCallback DIFF = new DiffUtil.ItemCallback() { + @Override + public boolean areItemsTheSame(Emoji a, Emoji b) { + return a.equals(b); + } + + @Override + public boolean areContentsTheSame(Emoji a, Emoji b) { + return a.equals(b); + } + }; + final Consumer callback; + + public EmojiSearchAdapter(final Consumer callback) { + super(DIFF); + this.callback = callback; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int position) { + return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.emoji_search_row, viewGroup, false)); } @Override - public View getView(int position, View view, ViewGroup parent) { - EmojiSearchRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.emoji_search_row, parent, false); - if (getItem(position) instanceof CustomEmoji) { - binding.nonunicode.setText(getItem(position).toInsert()); + public void onBindViewHolder(ViewHolder viewHolder, int position) { + final var binding = viewHolder.binding; + final var item = getItem(position); + if (item instanceof CustomEmoji) { + binding.nonunicode.setText(item.toInsert()); binding.nonunicode.setVisibility(View.VISIBLE); binding.unicode.setVisibility(View.GONE); } else { - binding.unicode.setText(getItem(position).toInsert()); + binding.unicode.setText(item.toInsert()); binding.unicode.setVisibility(View.VISIBLE); binding.nonunicode.setVisibility(View.GONE); } - binding.shortcode.setText(getItem(position).shortcodes.get(0)); - return binding.getRoot(); + binding.shortcode.setText(item.shortcodes.get(0)); + binding.getRoot().setOnClickListener(v -> { + callback.accept(item); + }); } - public void search(final String q) { + public void search(final Activity activity, final String q) { executor.execute(() -> { final List results = find(q); - ((Activity) getContext()).runOnUiThread(() -> { - clear(); - addAll(results); + activity.runOnUiThread(() -> { + submitList(results); notifyDataSetChanged(); }); }); } + + public static class ViewHolder extends RecyclerView.ViewHolder { + public final EmojiSearchRowBinding binding; + + private ViewHolder(EmojiSearchRowBinding binding) { + super(binding.getRoot()); + this.binding = binding; + } + } } } diff --git a/src/cheogram/res/layout/emoji_search.xml b/src/cheogram/res/layout/emoji_search.xml deleted file mode 100644 index 18e4a13928c83b8c22ec0a3d05dde10a9929caf6..0000000000000000000000000000000000000000 --- a/src/cheogram/res/layout/emoji_search.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - android:dividerHeight="0dp"> - - diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index ce52713f4cd5bd6a927a8f9ca73a8522626a1c7a..f65e07fb34ea8a62f53034a74673e51731fccb86 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -133,7 +133,6 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.crypto.axolotl.AxolotlService; import eu.siacs.conversations.crypto.axolotl.FingerprintStatus; -import eu.siacs.conversations.databinding.EmojiSearchBinding; import eu.siacs.conversations.databinding.FragmentConversationBinding; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Blockable; @@ -274,8 +273,6 @@ public class ConversationFragment extends XmppFragment private int identiconWidth = -1; private File savingAsSticker = null; private EmojiSearch emojiSearch = null; - private EmojiSearchBinding emojiSearchBinding = null; - private PopupWindow emojiPopup = null; private final OnClickListener clickToMuc = new OnClickListener() { @@ -1594,55 +1591,45 @@ public class ConversationFragment extends XmppFragment public void onPopupVisibilityChanged(boolean shown) {} }).build(); - final Pattern lastColonPattern = Pattern.compile("(? { - EmojiSearch.EmojiSearchAdapter adapter = ((EmojiSearch.EmojiSearchAdapter) emojiSearchBinding.emoji.getAdapter()); - Editable toInsert = adapter.getItem(position).toInsert(); - toInsert.append(" "); - Editable s = binding.textinput.getText(); - - Matcher lastColonMatcher = lastColonPattern.matcher(s); - int lastColon = -1; - while(lastColonMatcher.find()) lastColon = lastColonMatcher.end(); - if (lastColon > 0) s.replace(lastColon - 1, s.length(), toInsert, 0, toInsert.length()); - }); - setupEmojiSearch(); - int popupHeight = (int) displayMetrics.density * 200; - emojiPopup = new PopupWindow(emojiSearchBinding.getRoot(), WindowManager.LayoutParams.MATCH_PARENT, Math.min(popupHeight, displayMetrics.heightPixels > 0 ? displayMetrics.heightPixels / 5 : popupHeight)); Handler emojiDebounce = new Handler(Looper.getMainLooper()); - final Pattern notEmojiSearch = Pattern.compile("[^\\w\\(\\)\\+'\\-]"); - binding.textinput.addTextChangedListener(new TextWatcher() { - @Override - public void afterTextChanged(Editable s) { - emojiDebounce.removeCallbacksAndMessages(null); - emojiDebounce.postDelayed(() -> { - Matcher lastColonMatcher = lastColonPattern.matcher(s); - int lastColon = -1; - while(lastColonMatcher.find()) lastColon = lastColonMatcher.end(); - if (lastColon < 0) { - emojiPopup.dismiss(); - return; - } - final String q = s.toString().substring(lastColon); - if (notEmojiSearch.matcher(q).find()) { - emojiPopup.dismiss(); - } else { - EmojiSearch.EmojiSearchAdapter adapter = ((EmojiSearch.EmojiSearchAdapter) emojiSearchBinding.emoji.getAdapter()); - if (adapter != null) { - adapter.search(q); - emojiPopup.showAsDropDown(binding.textsend); - } - } - }, 400L); - } + setupEmojiSearch(); + Autocomplete.on(binding.textinput) + .with(activity.getDrawable(R.drawable.background_message_bubble)) + .with(new CharPolicy(':')) + .with(new RecyclerViewPresenter(activity) { + protected EmojiSearch.EmojiSearchAdapter adapter; - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { } + @Override + protected Adapter instantiateAdapter() { + adapter = emojiSearch.makeAdapter(item -> dispatchClick(item)); + return adapter; + } - @Override - public void onTextChanged(CharSequence s, int start, int count, int after) { } - }); + @Override + protected void onQuery(@Nullable CharSequence query) { + emojiDebounce.removeCallbacksAndMessages(null); + emojiDebounce.postDelayed(() -> { + if (getRecyclerView() == null) return; + getRecyclerView().getItemAnimator().endAnimations(); + adapter.search(activity, query.toString()); + }, 100L); + } + }) + .with(new AutocompleteCallback() { + @Override + public boolean onPopupItemClicked(Editable editable, EmojiSearch.Emoji emoji) { + int[] range = com.otaliastudios.autocomplete.CharPolicy.getQueryRange(editable); + if (range == null) return false; + range[0] -= 1; + final var toInsert = emoji.toInsert(); + toInsert.append(" "); + editable.replace(Math.max(0, range[0]), Math.min(editable.length(), range[1]), toInsert); + return true; + } + + @Override + public void onPopupVisibilityChanged(boolean shown) {} + }).build(); return binding.getRoot(); } @@ -1651,16 +1638,12 @@ public class ConversationFragment extends XmppFragment if (activity != null && activity.xmppConnectionService != null) { if (!activity.xmppConnectionService.getBooleanPreference("message_autocomplete", R.bool.message_autocomplete)) { emojiSearch = null; - if (emojiSearchBinding != null) emojiSearchBinding.emoji.setAdapter(null); return; } if (emojiSearch == null) { emojiSearch = activity.xmppConnectionService.emojiSearch(); } } - if (emojiSearch == null || emojiSearchBinding == null) return; - - emojiSearchBinding.emoji.setAdapter(emojiSearch.makeAdapter(activity)); } protected void newThreadTutorialToast(String s) { @@ -3225,7 +3208,6 @@ public class ConversationFragment extends XmppFragment this.activity.xmppConnectionService.getNotificationService().setOpenConversation(null); } this.reInitRequiredOnStart = true; - if (emojiPopup != null) emojiPopup.dismiss(); } private void updateChatState(final Conversation conversation, final String msg) {