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