1package com.cheogram.android;
2
3import android.app.Activity;
4import android.content.Context;
5import android.graphics.drawable.Drawable;
6import android.text.Spannable;
7import android.text.SpannableStringBuilder;
8import android.view.LayoutInflater;
9import android.view.View;
10import android.view.ViewGroup;
11
12import androidx.databinding.DataBindingUtil;
13import androidx.recyclerview.widget.DiffUtil;
14import androidx.recyclerview.widget.ListAdapter;
15import androidx.recyclerview.widget.RecyclerView;
16
17import com.google.common.collect.Lists;
18import com.google.common.io.CharStreams;
19
20import java.io.IOException;
21import java.io.InputStreamReader;
22import java.lang.Comparable;
23import java.util.ArrayList;
24import java.util.Arrays;
25import java.util.Collections;
26import java.util.List;
27import java.util.PriorityQueue;
28import java.util.Set;
29import java.util.TreeSet;
30import java.util.function.Consumer;
31
32import me.xdrop.fuzzywuzzy.FuzzySearch;
33import me.xdrop.fuzzywuzzy.model.BoundExtractedResult;
34
35import org.json.JSONArray;
36import org.json.JSONException;
37import org.json.JSONObject;
38
39import eu.siacs.conversations.R;
40import eu.siacs.conversations.databinding.EmojiSearchRowBinding;
41import eu.siacs.conversations.utils.ReplacingSerialSingleThreadExecutor;
42
43public class EmojiSearch {
44 protected final Set<Emoji> emoji = new TreeSet<>();
45
46 public EmojiSearch(Context context) {
47 try {
48 final JSONArray data = new JSONArray(CharStreams.toString(new InputStreamReader(context.getResources().openRawResource(R.raw.emoji), "UTF-8")));
49 for (int i = 0; i < data.length(); i++) {
50 emoji.add(new Emoji(data.getJSONObject(i)));
51 }
52 } catch (final JSONException | IOException e) {
53 throw new IllegalStateException("emoji.json invalid: " + e);
54 }
55 }
56
57 public synchronized void addEmoji(final Emoji one) {
58 emoji.add(one);
59 }
60
61 public synchronized List<Emoji> find(final String q) {
62 final ResultPQ pq = new ResultPQ();
63 for (Emoji e : emoji) {
64 if (e.emoticonMatch(q)) {
65 pq.addTopK(e, 999999, 10);
66 }
67 int shortcodeScore = e.shortcodes.isEmpty() ? 0 : Collections.max(Lists.transform(e.shortcodes, (shortcode) -> FuzzySearch.ratio(q, shortcode)));
68 int tagScore = e.tags.isEmpty() ? 0 : Collections.max(Lists.transform(e.tags, (tag) -> FuzzySearch.ratio(q, tag))) - 2;
69 pq.addTopK(e, Math.max(shortcodeScore, tagScore), 10);
70 }
71
72 for (BoundExtractedResult<Emoji> r : new ArrayList<>(pq)) {
73 for (Emoji e : emoji) {
74 if (e.shortcodeMatch(r.getReferent().uniquePart())) {
75 // hack see https://stackoverflow.com/questions/76880072/imagespan-with-emojicompat
76 e.shortcodes.clear();
77 e.shortcodes.addAll(r.getReferent().shortcodes);
78
79 pq.addTopK(e, r.getScore() - 1, 10);
80 }
81 }
82 }
83
84 List<Emoji> result = new ArrayList<>();
85 for (int i = 0; i < 10; i++) {
86 BoundExtractedResult<Emoji> e = pq.poll();
87 if (e != null) result.add(e.getReferent());
88 }
89 Collections.reverse(result);
90 return result;
91 }
92
93 public EmojiSearchAdapter makeAdapter(Consumer<Emoji> callback) {
94 return new EmojiSearchAdapter(callback);
95 }
96
97 public static class ResultPQ extends PriorityQueue<BoundExtractedResult<Emoji>> {
98 public void addTopK(Emoji e, int score, int k) {
99 BoundExtractedResult r = new BoundExtractedResult(e, null, score, 0);
100 if (size() < k) {
101 add(r);
102 } else if (r.compareTo(peek()) > 0) {
103 poll();
104 add(r);
105 }
106 }
107 }
108
109 public static class Emoji implements Comparable<Emoji> {
110 protected final String unicode;
111 protected final int order;
112 protected final List<String> tags = new ArrayList<>();
113 protected final List<String> emoticon = new ArrayList<>();
114 protected final List<String> shortcodes = new ArrayList<>();
115
116 public Emoji(final String unicode, final int order) {
117 this.unicode = unicode;
118 this.order = order;
119 }
120
121 public Emoji(JSONObject o) throws JSONException {
122 unicode = o.getString("unicode");
123 order = o.getInt("order");
124 final JSONArray rawTags = o.getJSONArray("tags");
125 for (int i = 0; i < rawTags.length(); i++) {
126 tags.add(rawTags.getString(i));
127 }
128 final JSONArray rawEmoticon = o.getJSONArray("emoticon");
129 for (int i = 0; i < rawEmoticon.length(); i++) {
130 emoticon.add(rawEmoticon.getString(i));
131 }
132 final JSONArray rawShortcodes = o.getJSONArray("shortcodes");
133 for (int i = 0; i < rawShortcodes.length(); i++) {
134 shortcodes.add(rawShortcodes.getString(i));
135 }
136 }
137
138 public boolean emoticonMatch(final String q) {
139 for (final String emote : emoticon) {
140 if (emote.equals(q) || emote.equals(":" + q)) return true;
141 }
142
143 return false;
144 }
145
146 public boolean shortcodeMatch(final String q) {
147 for (final String shortcode : shortcodes) {
148 if (shortcode.equals(q)) return true;
149 }
150
151 return false;
152 }
153
154 public SpannableStringBuilder toInsert() {
155 return new SpannableStringBuilder(unicode);
156 }
157
158 public String uniquePart() {
159 return unicode;
160 }
161
162 @Override
163 public int compareTo(Emoji o) {
164 if (equals(o)) return 0;
165 if (order == o.order) return uniquePart().compareTo(o.uniquePart());
166 return order - o.order;
167 }
168
169 @Override
170 public boolean equals(Object o) {
171 if (!(o instanceof Emoji)) return false;
172
173 return uniquePart().equals(((Emoji) o).uniquePart());
174 }
175 }
176
177 public static class CustomEmoji extends Emoji {
178 protected final String source;
179 protected final Drawable icon;
180
181 public CustomEmoji(final String shortcode, final String source, final Drawable icon, final String tag) {
182 super(null, 10);
183 shortcodes.add(shortcode);
184 if (tag != null) tags.add(tag);
185 this.source = source;
186 this.icon = icon;
187 if (icon == null) {
188 throw new IllegalArgumentException("icon must not be null");
189 }
190 }
191
192 public SpannableStringBuilder toInsert() {
193 SpannableStringBuilder builder = new SpannableStringBuilder(":" + shortcodes.get(0) + ":");
194 builder.setSpan(new InlineImageSpan(icon, source), 0, builder.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
195 return builder;
196 }
197
198 @Override
199 public String uniquePart() {
200 return source;
201 }
202 }
203
204 public class EmojiSearchAdapter extends ListAdapter<Emoji, EmojiSearchAdapter.ViewHolder> {
205 ReplacingSerialSingleThreadExecutor executor = new ReplacingSerialSingleThreadExecutor("EmojiSearchAdapter");
206
207 static final DiffUtil.ItemCallback<Emoji> DIFF = new DiffUtil.ItemCallback<Emoji>() {
208 @Override
209 public boolean areItemsTheSame(Emoji a, Emoji b) {
210 return a.equals(b);
211 }
212
213 @Override
214 public boolean areContentsTheSame(Emoji a, Emoji b) {
215 return a.equals(b);
216 }
217 };
218 final Consumer<Emoji> callback;
219
220 public EmojiSearchAdapter(final Consumer<Emoji> callback) {
221 super(DIFF);
222 this.callback = callback;
223 }
224
225 @Override
226 public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int position) {
227 return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.emoji_search_row, viewGroup, false));
228 }
229
230 @Override
231 public void onBindViewHolder(ViewHolder viewHolder, int position) {
232 final var binding = viewHolder.binding;
233 final var item = getItem(position);
234 if (item instanceof CustomEmoji) {
235 binding.nonunicode.setText(item.toInsert());
236 binding.nonunicode.setVisibility(View.VISIBLE);
237 binding.unicode.setVisibility(View.GONE);
238 } else {
239 binding.unicode.setText(item.toInsert());
240 binding.unicode.setVisibility(View.VISIBLE);
241 binding.nonunicode.setVisibility(View.GONE);
242 }
243 binding.shortcode.setText(item.shortcodes.get(0));
244 binding.getRoot().setOnClickListener(v -> {
245 callback.accept(item);
246 });
247 }
248
249 public void search(final Activity activity, final String q) {
250 executor.execute(() -> {
251 final List<Emoji> results = find(q);
252 activity.runOnUiThread(() -> {
253 submitList(results);
254 });
255 });
256 }
257
258 public static class ViewHolder extends RecyclerView.ViewHolder {
259 public final EmojiSearchRowBinding binding;
260
261 private ViewHolder(EmojiSearchRowBinding binding) {
262 super(binding.getRoot());
263 this.binding = binding;
264 }
265 }
266 }
267}