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
64 public Reaction(
65 final String reaction,
66 final Cid cid,
67 boolean received,
68 final Jid from,
69 final Jid trueJid,
70 final String occupantId) {
71 this.reaction = reaction;
72 this.cid = cid;
73 this.received = received;
74 this.from = from;
75 this.trueJid = trueJid;
76 this.occupantId = occupantId;
77 }
78
79 public static String toString(final Collection<Reaction> reactions) {
80 return (reactions == null || reactions.isEmpty()) ? null : GSON.toJson(reactions);
81 }
82
83 public static Collection<Reaction> fromString(final String asString) {
84 if (Strings.isNullOrEmpty(asString)) {
85 return Collections.emptyList();
86 }
87 try {
88 return GSON.fromJson(asString, new TypeToken<ArrayList<Reaction>>() {}.getType());
89 } catch (final JsonSyntaxException e) {
90 return Collections.emptyList();
91 }
92 }
93
94 public static Collection<Reaction> append(
95 final Collection<Reaction> existing,
96 final Collection<String> reactions,
97 final boolean received,
98 final Jid from,
99 final Jid trueJid,
100 final String occupantId) {
101 final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
102 builder.addAll(existing);
103 builder.addAll(
104 Collections2.transform(
105 reactions, r -> new Reaction(r, null, received, from, trueJid, occupantId)));
106 return builder.build();
107 }
108
109 public static Collection<Reaction> withOccupantId(
110 final Collection<Reaction> existing,
111 final Collection<String> reactions,
112 final boolean received,
113 final Jid from,
114 final Jid trueJid,
115 final String occupantId) {
116 final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
117 builder.addAll(Collections2.filter(existing, e -> !occupantId.equals(e.occupantId)));
118 builder.addAll(
119 Collections2.transform(
120 reactions, r -> new Reaction(r, null, received, from, trueJid, occupantId)));
121 return builder.build();
122 }
123
124 @NonNull
125 @Override
126 public String toString() {
127 return MoreObjects.toStringHelper(this)
128 .add("reaction", cid == null ? reaction : null)
129 .add("cid", cid)
130 .add("received", received)
131 .add("from", from)
132 .add("trueJid", trueJid)
133 .add("occupantId", occupantId)
134 .toString();
135 }
136
137 public int hashCode() {
138 return toString().hashCode();
139 }
140
141 public boolean equals(Object o) {
142 if (o == null) return false;
143 if (!(o instanceof Reaction)) return false;
144 return toString().equals(o.toString());
145 }
146
147 public static Collection<Reaction> withFrom(
148 final Collection<Reaction> existing,
149 final Collection<String> reactions,
150 final boolean received,
151 final Jid from) {
152 final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
153 builder.addAll(
154 Collections2.filter(existing, e -> !from.asBareJid().equals(e.from.asBareJid())));
155 builder.addAll(
156 Collections2.transform(
157 reactions, r -> new Reaction(r, null, received, from, null, null)));
158 return builder.build();
159 }
160
161 private static class JidTypeAdapter extends TypeAdapter<Jid> {
162 @Override
163 public void write(final JsonWriter out, final Jid value) throws IOException {
164 if (value == null) {
165 out.nullValue();
166 } else {
167 out.value(value.toEscapedString());
168 }
169 }
170
171 @Override
172 public Jid read(final JsonReader in) throws IOException {
173 if (in.peek() == JsonToken.NULL) {
174 in.nextNull();
175 return null;
176 } else if (in.peek() == JsonToken.STRING) {
177 final String value = in.nextString();
178 return Jid.ofEscaped(value);
179 }
180 throw new IOException("Unexpected token");
181 }
182 }
183
184 private static class CidTypeAdapter extends TypeAdapter<Cid> {
185 @Override
186 public void write(final JsonWriter out, final Cid value) throws IOException {
187 if (value == null) {
188 out.nullValue();
189 } else {
190 out.value(value.toString());
191 }
192 }
193
194 @Override
195 public Cid read(final JsonReader in) throws IOException {
196 if (in.peek() == JsonToken.NULL) {
197 in.nextNull();
198 return null;
199 } else if (in.peek() == JsonToken.STRING) {
200 final String value = in.nextString();
201 return Cid.decode(value);
202 }
203 throw new IOException("Unexpected token");
204 }
205 }
206
207 public static Aggregated aggregated(final Collection<Reaction> reactions) {
208 return aggregated(reactions, (r) -> null);
209 }
210
211 public static Aggregated aggregated(final Collection<Reaction> reactions, Function<Reaction, GetThumbnailForCid> thumbnailer) {
212 final Map<EmojiSearch.Emoji, Collection<Reaction>> aggregatedReactions =
213 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();
214 final List<Map.Entry<EmojiSearch.Emoji, Collection<Reaction>>> sortedList =
215 Ordering.from(
216 Comparator.comparingInt(
217 (Map.Entry<EmojiSearch.Emoji, Collection<Reaction>> o) -> o.getValue().size()))
218 .reverse()
219 .immutableSortedCopy(aggregatedReactions.entrySet());
220 return new Aggregated(
221 sortedList,
222 ImmutableSet.copyOf(
223 Collections2.transform(
224 Collections2.filter(reactions, r -> r.cid == null && !r.received),
225 r -> r.reaction)));
226 }
227
228 public static final class Aggregated {
229
230 public final List<Map.Entry<EmojiSearch.Emoji, Collection<Reaction>>> reactions;
231 public final Set<String> ourReactions;
232
233 private Aggregated(
234 final List<Map.Entry<EmojiSearch.Emoji, Collection<Reaction>>> reactions, Set<String> ourReactions) {
235 this.reactions = reactions;
236 this.ourReactions = ourReactions;
237 }
238 }
239}