1package eu.siacs.conversations.entities;
2
3import android.util.Log;
4
5import androidx.annotation.NonNull;
6
7import com.cheogram.android.EmojiSearch;
8import com.cheogram.android.GetThumbnailForCid;
9
10import com.google.common.base.MoreObjects;
11import com.google.common.base.Strings;
12import com.google.common.collect.Collections2;
13import com.google.common.collect.ImmutableList;
14import com.google.common.collect.ImmutableSet;
15import com.google.common.collect.Maps;
16import com.google.common.collect.Multimaps;
17import com.google.common.collect.Ordering;
18import com.google.gson.Gson;
19import com.google.gson.GsonBuilder;
20import com.google.gson.JsonSyntaxException;
21import com.google.gson.TypeAdapter;
22import com.google.gson.reflect.TypeToken;
23import com.google.gson.stream.JsonReader;
24import com.google.gson.stream.JsonToken;
25import com.google.gson.stream.JsonWriter;
26
27import io.ipfs.cid.Cid;
28
29import eu.siacs.conversations.Config;
30import eu.siacs.conversations.utils.Emoticons;
31import eu.siacs.conversations.xmpp.Jid;
32
33import java.io.IOException;
34import java.util.Arrays;
35import java.util.Collection;
36import java.util.Collections;
37import java.util.Comparator;
38import java.util.List;
39import java.util.Map;
40import java.util.Set;
41import java.util.function.Function;
42
43public class Reaction {
44
45 public static final List<String> SUGGESTIONS =
46 Arrays.asList(
47 "\u2764\uFE0F",
48 "\uD83D\uDC4D",
49 "\uD83D\uDC4E",
50 "\uD83D\uDE02",
51 "\u2757",
52 "\u2753");
53
54 private static final Gson GSON;
55
56 static {
57 GSON = new GsonBuilder().registerTypeAdapter(Jid.class, new JidTypeAdapter()).registerTypeAdapter(Cid.class, new CidTypeAdapter()).create();
58 }
59
60 public final String reaction;
61 public final boolean received;
62 public final Jid from;
63 public final Jid trueJid;
64 public final String occupantId;
65 public final Cid cid;
66 public final String envelopeId;
67
68 public Reaction(
69 final String reaction,
70 final Cid cid,
71 boolean received,
72 final Jid from,
73 final Jid trueJid,
74 final String occupantId,
75 final String envelopeId) {
76 this.reaction = reaction;
77 this.cid = cid;
78 this.received = received;
79 this.from = from;
80 this.trueJid = trueJid;
81 this.occupantId = occupantId;
82 this.envelopeId = envelopeId;
83 }
84
85 public String normalizedReaction() {
86 return Emoticons.normalizeToVS16(this.reaction);
87 }
88
89 public static String toString(final Collection<Reaction> reactions) {
90 return (reactions == null || reactions.isEmpty()) ? null : GSON.toJson(reactions);
91 }
92
93 public static Collection<Reaction> fromString(final String asString) {
94 if (Strings.isNullOrEmpty(asString)) {
95 return Collections.emptyList();
96 }
97 try {
98 return GSON.fromJson(asString, new TypeToken<List<Reaction>>() {}.getType());
99 } catch (final IllegalArgumentException | JsonSyntaxException e) {
100 Log.e(Config.LOGTAG, "could not restore reactions", e);
101 return Collections.emptyList();
102 }
103 }
104
105 public static Collection<Reaction> withMine(
106 final Collection<Reaction> existing,
107 final Collection<String> reactions,
108 final boolean received,
109 final Jid from,
110 final Jid trueJid,
111 final String occupantId,
112 final String envelopeId) {
113 final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
114 builder.addAll(Collections2.filter(existing, e -> e.received));
115 builder.addAll(
116 Collections2.transform(
117 reactions, r -> new Reaction(r, null, received, from, trueJid, occupantId, envelopeId)));
118 return builder.build();
119 }
120
121 public static Collection<Reaction> withOccupantId(
122 final Collection<Reaction> existing,
123 final Collection<String> reactions,
124 final boolean received,
125 final Jid from,
126 final Jid trueJid,
127 final String occupantId,
128 final String envelopeId) {
129 final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
130 builder.addAll(Collections2.filter(existing, e -> !occupantId.equals(e.occupantId)));
131 builder.addAll(
132 Collections2.transform(
133 reactions, r -> new Reaction(r, null, received, from, trueJid, occupantId, envelopeId)));
134 return builder.build();
135 }
136
137 @NonNull
138 @Override
139 public String toString() {
140 return MoreObjects.toStringHelper(this)
141 .add("reaction", cid == null ? reaction : null)
142 .add("cid", cid)
143 .add("received", received)
144 .add("from", from)
145 .add("trueJid", trueJid)
146 .add("occupantId", occupantId)
147 .toString();
148 }
149
150 public int hashCode() {
151 return toString().hashCode();
152 }
153
154 public boolean equals(Object o) {
155 if (o == null) return false;
156 if (!(o instanceof Reaction)) return false;
157 return toString().equals(o.toString());
158 }
159
160 public static Collection<Reaction> withFrom(
161 final Collection<Reaction> existing,
162 final Collection<String> reactions,
163 final boolean received,
164 final Jid from,
165 final String envelopeId) {
166 final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
167 builder.addAll(
168 Collections2.filter(existing, e -> !from.asBareJid().equals(e.from.asBareJid())));
169 builder.addAll(
170 Collections2.transform(
171 reactions, r -> new Reaction(r, null, received, from, null, null, envelopeId)));
172 return builder.build();
173 }
174
175 private static class JidTypeAdapter extends TypeAdapter<Jid> {
176 @Override
177 public void write(final JsonWriter out, final Jid value) throws IOException {
178 if (value == null) {
179 out.nullValue();
180 } else {
181 out.value(value.toEscapedString());
182 }
183 }
184
185 @Override
186 public Jid read(final JsonReader in) throws IOException {
187 if (in.peek() == JsonToken.NULL) {
188 in.nextNull();
189 return null;
190 } else if (in.peek() == JsonToken.STRING) {
191 final String value = in.nextString();
192 return Jid.ofEscaped(value);
193 }
194 throw new IOException("Unexpected token");
195 }
196 }
197
198 private static class CidTypeAdapter extends TypeAdapter<Cid> {
199 @Override
200 public void write(final JsonWriter out, final Cid value) throws IOException {
201 if (value == null) {
202 out.nullValue();
203 } else {
204 out.value(value.toString());
205 }
206 }
207
208 @Override
209 public Cid read(final JsonReader in) throws IOException {
210 if (in.peek() == JsonToken.NULL) {
211 in.nextNull();
212 return null;
213 } else if (in.peek() == JsonToken.STRING) {
214 final String value = in.nextString();
215 return Cid.decode(value);
216 }
217 throw new IOException("Unexpected token");
218 }
219 }
220
221 public static Aggregated aggregated(final Collection<Reaction> reactions) {
222 return aggregated(reactions, (r) -> null);
223 }
224
225 public static Aggregated aggregated(final Collection<Reaction> reactions, Function<Reaction, GetThumbnailForCid> thumbnailer) {
226 final Map<EmojiSearch.Emoji, Collection<Reaction>> aggregatedReactions =
227 Multimaps.index(reactions, r -> r.cid == null ? new EmojiSearch.Emoji(r.reaction, 0) : new EmojiSearch.CustomEmoji(r.reaction, r.cid.toString(), thumbnailer.apply(r).getThumbnail(r.cid), null)).asMap();
228 final List<Map.Entry<EmojiSearch.Emoji, Collection<Reaction>>> sortedList =
229 Ordering.from(
230 Comparator.comparingInt(
231 (Map.Entry<EmojiSearch.Emoji, Collection<Reaction>> o) -> o.getValue().size()))
232 .reverse()
233 .immutableSortedCopy(aggregatedReactions.entrySet());
234 return new Aggregated(
235 sortedList,
236 ImmutableSet.copyOf(
237 Collections2.transform(
238 Collections2.filter(reactions, r -> r.cid == null && !r.received),
239 Reaction::normalizedReaction)));
240 }
241
242 public static final class Aggregated {
243
244 public final List<Map.Entry<EmojiSearch.Emoji, Collection<Reaction>>> reactions;
245 public final Set<String> ourReactions;
246
247 private Aggregated(
248 final List<Map.Entry<EmojiSearch.Emoji, Collection<Reaction>>> reactions, Set<String> ourReactions) {
249 this.reactions = reactions;
250 this.ourReactions = ourReactions;
251 }
252 }
253}