Detailed changes
@@ -120,6 +120,7 @@ dependencies {
implementation 'org.tomlj:tomlj:1.1.0'
implementation 'com.tbuonomo:dotsindicator:4.2'
implementation 'com.github.Priyansh-Kedia:OpenGraphParser:2.5.5'
+ implementation 'me.xdrop:fuzzywuzzy:1.4.0'
// INSERT
}
@@ -0,0 +1,153 @@
+package com.cheogram.android;
+import android.util.Log;
+
+import android.content.Context;
+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 com.google.common.collect.Lists;
+import com.google.common.io.CharStreams;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.Comparable;
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import me.xdrop.fuzzywuzzy.FuzzySearch;
+import me.xdrop.fuzzywuzzy.algorithms.WeightedRatio;
+import me.xdrop.fuzzywuzzy.model.BoundExtractedResult;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.databinding.EmojiSearchRowBinding;
+
+public class EmojiSearch {
+ protected final Set<Emoji> emoji = new TreeSet<>();
+
+ public EmojiSearch(Context context) {
+ try {
+ final JSONArray data = new JSONArray(CharStreams.toString(new InputStreamReader(context.getResources().openRawResource(R.raw.emoji), "UTF-8")));
+ for (int i = 0; i < data.length(); i++) {
+ emoji.add(new Emoji(data.getJSONObject(i)));
+ }
+ } catch (final JSONException | IOException e) {
+ throw new IllegalStateException("emoji.json invalid: " + e);
+ }
+ }
+
+ public List<Emoji> find(final String q) {
+ final Set<Emoji> emoticon = new TreeSet<>();
+ for (Emoji e : emoji) {
+ if (e.emoticonMatch(q)) {
+ emoticon.add(e);
+ }
+ }
+
+ WeightedRatio wr = new WeightedRatio();
+ List<BoundExtractedResult<Emoji>> result = FuzzySearch.extractTop(
+ q,
+ emoji,
+ (e) -> e.fuzzyFind,
+ (query, s) -> {
+ int score = 0;
+ String[] kinds = s.split(">");
+ for (int i = 0; i < kinds.length; i++) {
+ int nscore = Collections.max(Lists.transform(Arrays.asList(kinds[i].split("~")), (x) -> wr.apply(query, x))) - (i * 2);
+ if (nscore > score) score = nscore;
+ }
+ return score;
+ },
+ 10
+ );
+
+ List<Emoji> lst = new ArrayList<>(emoticon);
+ lst.addAll(Lists.transform(result, (r) -> r.getReferent()));
+ return lst;
+ }
+
+ public EmojiSearchAdapter makeAdapter(Context context) {
+ return new EmojiSearchAdapter(context);
+ }
+
+ public static class Emoji implements Comparable<Emoji> {
+ protected final String unicode;
+ protected final int order;
+ protected final List<String> tags = new ArrayList<>();
+ protected final List<String> emoticon = new ArrayList<>();
+ protected final List<String> shortcodes = new ArrayList<>();
+ protected final String fuzzyFind;
+
+ public Emoji(JSONObject o) throws JSONException {
+ unicode = o.getString("unicode");
+ order = o.getInt("order");
+ final JSONArray rawTags = o.getJSONArray("tags");
+ for (int i = 0; i < rawTags.length(); i++) {
+ tags.add(rawTags.getString(i));
+ }
+ final JSONArray rawEmoticon = o.getJSONArray("emoticon");
+ for (int i = 0; i < rawEmoticon.length(); i++) {
+ emoticon.add(rawEmoticon.getString(i));
+ }
+ final JSONArray rawShortcodes = o.getJSONArray("shortcodes");
+ for (int i = 0; i < rawShortcodes.length(); i++) {
+ shortcodes.add(rawShortcodes.getString(i));
+ }
+ fuzzyFind = String.join("~", shortcodes) + ">" + String.join("~", tags);
+ }
+
+ public boolean emoticonMatch(final String q) {
+ for (final String emote : emoticon) {
+ if (emote.equals(q) || emote.equals(":" + q)) return true;
+ }
+
+ return false;
+ }
+
+ public SpannableStringBuilder toInsert() {
+ return new SpannableStringBuilder(unicode);
+ }
+
+ public int compareTo(Emoji o) {
+ if (equals(o)) return 0;
+ if (order == o.order) return -1;
+ return order - o.order;
+ }
+
+ public boolean equals(Emoji o) {
+ return toInsert().equals(o.toInsert());
+ }
+ }
+
+ public class EmojiSearchAdapter extends ArrayAdapter<Emoji> {
+ public EmojiSearchAdapter(Context context) {
+ super(context, 0);
+ }
+
+ @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);
+ binding.unicode.setText(getItem(position).toInsert());
+ binding.shortcode.setText(getItem(position).shortcodes.get(0));
+ return binding.getRoot();
+ }
+
+ public void search(final String q) {
+ clear();
+ addAll(find(q));
+ notifyDataSetChanged();
+ }
+ }
+}
@@ -0,0 +1,16 @@
+<?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:background="?attr/color_background_secondary"
+ android:divider="@android:color/transparent"
+ android:dividerHeight="0dp"></ListView>
+
+</layout>
@@ -0,0 +1,35 @@
+<?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">
+
+ <LinearLayout
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:layout_centerVertical="true"
+ android:orientation="horizontal"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:paddingLeft="@dimen/avatar_item_distance"
+ android:paddingRight="@dimen/avatar_item_distance"
+ android:background="@drawable/list_choice">
+
+ <TextView
+ android:id="@+id/unicode"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center_vertical"
+ android:textAppearance="@style/TextAppearance.Conversations.Body1"
+ android:textColor="?attr/edit_text_color" />
+
+ <TextView
+ android:id="@+id/shortcode"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingLeft="8dp"
+ android:gravity="center_vertical"
+ android:textAppearance="@style/TextAppearance.Conversations.Body1"
+ android:textColor="?attr/edit_text_color" />
+
+ </LinearLayout>
+
+</layout>
@@ -0,0 +1 @@
@@ -56,6 +56,7 @@ import androidx.core.app.RemoteInput;
import androidx.core.content.ContextCompat;
import androidx.core.util.Consumer;
+import com.cheogram.android.EmojiSearch;
import com.cheogram.android.WebxdcUpdate;
import com.google.common.base.Objects;
@@ -515,6 +516,7 @@ public class XmppConnectionService extends Service {
private LruCache<String, Drawable> mDrawableCache;
private final BroadcastReceiver mInternalEventReceiver = new InternalEventReceiver();
private final BroadcastReceiver mInternalScreenEventReceiver = new InternalEventReceiver();
+ private EmojiSearch emojiSearch = null;
private static String generateFetchKey(Account account, final Avatar avatar) {
return account.getJid().asBareJid() + "_" + avatar.owner + "_" + avatar.sha1sum;
@@ -718,6 +720,10 @@ public class XmppConnectionService extends Service {
});
}
+ public EmojiSearch emojiSearch() {
+ return emojiSearch;
+ }
+
public Conversation find(Bookmark bookmark) {
return find(bookmark.getAccount(), bookmark.getJid());
}
@@ -1262,6 +1268,7 @@ public class XmppConnectionService extends Service {
@Override
public void onCreate() {
LibIdnXmppStringprep.setup();
+ emojiSearch = new EmojiSearch(this);
setTheme(ThemeHelper.find(this));
ThemeHelper.applyCustomColors(this);
if (Compatibility.runsTwentySix()) {
@@ -25,6 +25,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
+import android.os.Looper;
import android.os.storage.StorageManager;
import android.os.SystemClock;
import android.preference.PreferenceManager;
@@ -32,6 +33,7 @@ import android.provider.MediaStore;
import android.text.Editable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
+import android.text.TextWatcher;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
@@ -46,6 +48,7 @@ import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
+import android.view.WindowManager;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.AdapterView;
@@ -53,6 +56,7 @@ import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.CheckBox;
import android.widget.ListView;
import android.widget.PopupMenu;
+import android.widget.PopupWindow;
import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;
@@ -72,6 +76,7 @@ import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
import com.cheogram.android.BobTransfer;
+import com.cheogram.android.EmojiSearch;
import com.cheogram.android.WebxdcPage;
import com.google.common.base.Optional;
@@ -101,6 +106,7 @@ 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;
@@ -223,6 +229,8 @@ public class ConversationFragment extends XmppFragment
private boolean reInitRequiredOnStart = true;
private int identiconWidth = -1;
private File savingAsSticker = null;
+ private EmojiSearch emojiSearch = null;
+ private EmojiSearchBinding emojiSearchBinding = null;
private final OnClickListener clickToMuc =
new OnClickListener() {
@@ -1363,9 +1371,60 @@ public class ConversationFragment extends XmppFragment
return true;
});
+ 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();
+ int lastColon = s.toString().lastIndexOf(':');
+ s.replace(lastColon, s.length(), toInsert, 0, toInsert.length());
+ });
+ setupEmojiSearch();
+ PopupWindow emojiPopup = new PopupWindow(emojiSearchBinding.getRoot(), WindowManager.LayoutParams.MATCH_PARENT, 400);
+ Handler emojiDebounce = new Handler(Looper.getMainLooper());
+ binding.textinput.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void afterTextChanged(Editable s) {
+ emojiDebounce.removeCallbacksAndMessages(null);
+ emojiDebounce.postDelayed(() -> {
+ int lastColon = s.toString().lastIndexOf(':');
+ if (lastColon < 0) {
+ emojiPopup.dismiss();
+ return;
+ }
+ final String q = s.toString().substring(lastColon + 1);
+ if (q.matches(".*[^\\w\\(\\)\\+'\\-].*")) {
+ emojiPopup.dismiss();
+ } else {
+ EmojiSearch.EmojiSearchAdapter adapter = ((EmojiSearch.EmojiSearchAdapter) emojiSearchBinding.emoji.getAdapter());
+ if (adapter != null) {
+ adapter.search(q);
+ emojiPopup.showAsDropDown(binding.textinput);
+ }
+ }
+ }, 300L);
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int count, int after) { }
+ });
+
return binding.getRoot();
}
+ protected void setupEmojiSearch() {
+ if (emojiSearch == null && activity != null && activity.xmppConnectionService != null) {
+ emojiSearch = activity.xmppConnectionService.emojiSearch();
+ }
+ if (emojiSearch == null || emojiSearchBinding == null) return;
+
+ emojiSearchBinding.emoji.setAdapter(emojiSearch.makeAdapter(activity));
+ }
+
protected void newThreadTutorialToast(String s) {
final SharedPreferences p = PreferenceManager.getDefaultSharedPreferences(activity);
final int tutorialCount = p.getInt("thread_tutorial", 0);
@@ -3939,6 +3998,7 @@ public class ConversationFragment extends XmppFragment
@Override
public void onBackendConnected() {
Log.d(Config.LOGTAG, "ConversationFragment.onBackendConnected()");
+ setupEmojiSearch();
String uuid = pendingConversationsUuid.pop();
if (uuid != null) {
if (!findAndReInitByUuidOrArchive(uuid)) {