1package eu.siacs.conversations.entities;
2
3import androidx.annotation.NonNull;
4
5import com.google.common.base.MoreObjects;
6import com.google.common.base.Strings;
7import com.google.common.collect.Collections2;
8import com.google.common.collect.ImmutableList;
9import com.google.common.collect.ImmutableSet;
10import com.google.common.collect.Maps;
11import com.google.common.collect.Multimaps;
12import com.google.common.collect.Ordering;
13import com.google.gson.reflect.TypeToken;
14import com.google.gson.Gson;
15import com.google.gson.GsonBuilder;
16import com.google.gson.JsonSyntaxException;
17import com.google.gson.TypeAdapter;
18import com.google.gson.stream.JsonReader;
19import com.google.gson.stream.JsonToken;
20import com.google.gson.stream.JsonWriter;
21
22import eu.siacs.conversations.xmpp.Jid;
23
24import java.io.IOException;
25import java.util.ArrayList;
26import java.util.Arrays;
27import java.util.Collection;
28import java.util.Collections;
29import java.util.Comparator;
30import java.util.List;
31import java.util.Map;
32import java.util.Set;
33
34public class Reaction {
35
36 public static final List<String> SUGGESTIONS =
37 Arrays.asList(
38 "\u2764\uFE0F",
39 "\uD83D\uDC4D",
40 "\uD83D\uDC4E",
41 "\uD83D\uDE02",
42 "\u2757",
43 "\u2753");
44
45 private static final Gson GSON;
46
47 static {
48 GSON = new GsonBuilder().registerTypeAdapter(Jid.class, new JidTypeAdapter()).create();
49 }
50
51 public final String reaction;
52 public final boolean received;
53 public final Jid from;
54 public final Jid trueJid;
55 public final String occupantId;
56
57 public Reaction(
58 final String reaction,
59 boolean received,
60 final Jid from,
61 final Jid trueJid,
62 final String occupantId) {
63 this.reaction = reaction;
64 this.received = received;
65 this.from = from;
66 this.trueJid = trueJid;
67 this.occupantId = occupantId;
68 }
69
70 public static String toString(final Collection<Reaction> reactions) {
71 return (reactions == null || reactions.isEmpty()) ? null : GSON.toJson(reactions);
72 }
73
74 public static Collection<Reaction> fromString(final String asString) {
75 if (Strings.isNullOrEmpty(asString)) {
76 return Collections.emptyList();
77 }
78 try {
79 return GSON.fromJson(asString, new TypeToken<ArrayList<Reaction>>() {}.getType());
80 } catch (final JsonSyntaxException e) {
81 return Collections.emptyList();
82 }
83 }
84
85 public static Collection<Reaction> append(
86 final Collection<Reaction> existing,
87 final Collection<String> reactions,
88 final boolean received,
89 final Jid from,
90 final Jid trueJid,
91 final String occupantId) {
92 final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
93 builder.addAll(existing);
94 builder.addAll(
95 Collections2.transform(
96 reactions, r -> new Reaction(r, received, from, trueJid, occupantId)));
97 return builder.build();
98 }
99
100 public static Collection<Reaction> withOccupantId(
101 final Collection<Reaction> existing,
102 final Collection<String> reactions,
103 final boolean received,
104 final Jid from,
105 final Jid trueJid,
106 final String occupantId) {
107 final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
108 builder.addAll(Collections2.filter(existing, e -> !occupantId.equals(e.occupantId)));
109 builder.addAll(
110 Collections2.transform(
111 reactions, r -> new Reaction(r, received, from, trueJid, occupantId)));
112 return builder.build();
113 }
114
115 @NonNull
116 @Override
117 public String toString() {
118 return MoreObjects.toStringHelper(this)
119 .add("reaction", reaction)
120 .add("received", received)
121 .add("from", from)
122 .add("trueJid", trueJid)
123 .add("occupantId", occupantId)
124 .toString();
125 }
126
127 public int hashCode() {
128 return toString().hashCode();
129 }
130
131 public boolean equals(Object o) {
132 if (o == null) return false;
133 if (!(o instanceof Reaction)) return false;
134 return toString().equals(o.toString());
135 }
136
137 public static Collection<Reaction> withFrom(
138 final Collection<Reaction> existing,
139 final Collection<String> reactions,
140 final boolean received,
141 final Jid from) {
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, received, from, null, null)));
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 public static Aggregated aggregated(final Collection<Reaction> reactions) {
175 final Map<String, Integer> aggregatedReactions =
176 Maps.transformValues(
177 Multimaps.index(reactions, r -> r.reaction).asMap(), Collection::size);
178 final List<Map.Entry<String, Integer>> sortedList =
179 Ordering.from(
180 Comparator.comparingInt(
181 (Map.Entry<String, Integer> o) -> o.getValue()))
182 .reverse()
183 .immutableSortedCopy(aggregatedReactions.entrySet());
184 return new Aggregated(
185 sortedList,
186 ImmutableSet.copyOf(
187 Collections2.transform(
188 Collections2.filter(reactions, r -> !r.received),
189 r -> r.reaction)));
190 }
191
192 public static final class Aggregated {
193
194 public final List<Map.Entry<String, Integer>> reactions;
195 public final Set<String> ourReactions;
196
197 private Aggregated(
198 final List<Map.Entry<String, Integer>> reactions, Set<String> ourReactions) {
199 this.reactions = reactions;
200 this.ourReactions = ourReactions;
201 }
202 }
203}