@@ -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<Emoji> callback) {
+ return new EmojiSearchAdapter(callback);
}
public static class ResultPQ extends PriorityQueue<BoundExtractedResult<Emoji>> {
@@ -198,38 +201,68 @@ public class EmojiSearch {
}
}
- public class EmojiSearchAdapter extends ArrayAdapter<Emoji> {
+ public class EmojiSearchAdapter extends ListAdapter<Emoji, EmojiSearchAdapter.ViewHolder> {
ReplacingSerialSingleThreadExecutor executor = new ReplacingSerialSingleThreadExecutor("EmojiSearchAdapter");
- public EmojiSearchAdapter(Activity context) {
- super(context, 0);
+ static final DiffUtil.ItemCallback<Emoji> DIFF = new DiffUtil.ItemCallback<Emoji>() {
+ @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<Emoji> callback;
+
+ public EmojiSearchAdapter(final Consumer<Emoji> 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<Emoji> 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;
+ }
+ }
}
}
@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<layout xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:app="http://schemas.android.com/apk/res-auto">
-
- <ListView
- android:id="@+id/emoji"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:layout_alignParentStart="true"
- android:layout_alignParentLeft="true"
- android:layout_alignParentTop="true"
- android:divider="@android:color/transparent"
- android:background="?colorSurfaceVariant">
- android:dividerHeight="0dp"></ListView>
-
-</layout>
@@ -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("(?<!\\w):");
- emojiSearchBinding = DataBindingUtil.inflate(inflater, R.layout.emoji_search, null, false);
- emojiSearchBinding.emoji.setOnItemClickListener((parent, view, position, id) -> {
- 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.<EmojiSearch.Emoji>on(binding.textinput)
+ .with(activity.getDrawable(R.drawable.background_message_bubble))
+ .with(new CharPolicy(':'))
+ .with(new RecyclerViewPresenter<EmojiSearch.Emoji>(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<EmojiSearch.Emoji>() {
+ @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) {