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