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