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> withOccupantId(
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 -> !occupantId.equals(e.occupantId)));
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 @NonNull
114 @Override
115 public String toString() {
116 return MoreObjects.toStringHelper(this)
117 .add("reaction", cid == null ? reaction : null)
118 .add("cid", cid)
119 .add("received", received)
120 .add("from", from)
121 .add("trueJid", trueJid)
122 .add("occupantId", occupantId)
123 .toString();
124 }
125
126 public int hashCode() {
127 return toString().hashCode();
128 }
129
130 public boolean equals(Object o) {
131 if (o == null) return false;
132 if (!(o instanceof Reaction)) return false;
133 return toString().equals(o.toString());
134 }
135
136 public static Collection<Reaction> withFrom(
137 final Collection<Reaction> existing,
138 final Collection<String> reactions,
139 final boolean received,
140 final Jid from,
141 final String envelopeId) {
142 final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
143 builder.addAll(
144 Collections2.filter(existing, e -> !from.asBareJid().equals(e.from.asBareJid())));
145 builder.addAll(
146 Collections2.transform(
147 reactions, r -> new Reaction(r, null, received, from, null, null, envelopeId)));
148 return builder.build();
149 }
150
151 private static class JidTypeAdapter extends TypeAdapter<Jid> {
152 @Override
153 public void write(final JsonWriter out, final Jid value) throws IOException {
154 if (value == null) {
155 out.nullValue();
156 } else {
157 out.value(value.toEscapedString());
158 }
159 }
160
161 @Override
162 public Jid read(final JsonReader in) throws IOException {
163 if (in.peek() == JsonToken.NULL) {
164 in.nextNull();
165 return null;
166 } else if (in.peek() == JsonToken.STRING) {
167 final String value = in.nextString();
168 return Jid.ofEscaped(value);
169 }
170 throw new IOException("Unexpected token");
171 }
172 }
173
174 private static class CidTypeAdapter extends TypeAdapter<Cid> {
175 @Override
176 public void write(final JsonWriter out, final Cid value) throws IOException {
177 if (value == null) {
178 out.nullValue();
179 } else {
180 out.value(value.toString());
181 }
182 }
183
184 @Override
185 public Cid read(final JsonReader in) throws IOException {
186 if (in.peek() == JsonToken.NULL) {
187 in.nextNull();
188 return null;
189 } else if (in.peek() == JsonToken.STRING) {
190 final String value = in.nextString();
191 return Cid.decode(value);
192 }
193 throw new IOException("Unexpected token");
194 }
195 }
196
197 public static Aggregated aggregated(final Collection<Reaction> reactions) {
198 return aggregated(reactions, (r) -> null);
199 }
200
201 public static Aggregated aggregated(final Collection<Reaction> reactions, Function<Reaction, GetThumbnailForCid> thumbnailer) {
202 final Map<EmojiSearch.Emoji, Collection<Reaction>> aggregatedReactions =
203 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();
204 final List<Map.Entry<EmojiSearch.Emoji, Collection<Reaction>>> sortedList =
205 Ordering.from(
206 Comparator.comparingInt(
207 (Map.Entry<EmojiSearch.Emoji, Collection<Reaction>> o) -> o.getValue().size()))
208 .reverse()
209 .immutableSortedCopy(aggregatedReactions.entrySet());
210 return new Aggregated(
211 sortedList,
212 ImmutableSet.copyOf(
213 Collections2.transform(
214 Collections2.filter(reactions, r -> r.cid == null && !r.received),
215 r -> r.reaction)));
216 }
217
218 public static final class Aggregated {
219
220 public final List<Map.Entry<EmojiSearch.Emoji, Collection<Reaction>>> reactions;
221 public final Set<String> ourReactions;
222
223 private Aggregated(
224 final List<Map.Entry<EmojiSearch.Emoji, Collection<Reaction>>> reactions, Set<String> ourReactions) {
225 this.reactions = reactions;
226 this.ourReactions = ourReactions;
227 }
228 }
229}