1package eu.siacs.conversations.entities;
2
3import android.util.Log;
4
5import androidx.annotation.NonNull;
6
7import com.google.common.base.MoreObjects;
8import com.google.common.base.Strings;
9import com.google.common.collect.Collections2;
10import com.google.common.collect.ImmutableList;
11import com.google.common.collect.ImmutableSet;
12import com.google.common.collect.Maps;
13import com.google.common.collect.Multimaps;
14import com.google.common.collect.Ordering;
15import com.google.gson.Gson;
16import com.google.gson.GsonBuilder;
17import com.google.gson.JsonSyntaxException;
18import com.google.gson.TypeAdapter;
19import com.google.gson.reflect.TypeToken;
20import com.google.gson.stream.JsonReader;
21import com.google.gson.stream.JsonToken;
22import com.google.gson.stream.JsonWriter;
23
24import eu.siacs.conversations.Config;
25import eu.siacs.conversations.utils.Emoticons;
26import eu.siacs.conversations.xmpp.Jid;
27
28import java.io.IOException;
29import java.util.Arrays;
30import java.util.Collection;
31import java.util.Collections;
32import java.util.Comparator;
33import java.util.List;
34import java.util.Map;
35import java.util.Set;
36
37public class Reaction {
38
39 public static final List<String> SUGGESTIONS =
40 Arrays.asList(
41 "\u2764\uFE0F",
42 "\uD83D\uDC4D",
43 "\uD83D\uDC4E",
44 "\uD83D\uDE02",
45 "\uD83D\uDE2E",
46 "\uD83D\uDE22");
47
48 private static final Gson GSON;
49
50 static {
51 GSON = new GsonBuilder().registerTypeAdapter(Jid.class, new JidTypeAdapter()).create();
52 }
53
54 public final String reaction;
55 public final boolean received;
56 public final Jid from;
57 public final Jid trueJid;
58 public final String occupantId;
59
60 public Reaction(
61 final String reaction,
62 boolean received,
63 final Jid from,
64 final Jid trueJid,
65 final String occupantId) {
66 this.reaction = reaction;
67 this.received = received;
68 this.from = from;
69 this.trueJid = trueJid;
70 this.occupantId = occupantId;
71 }
72
73 public String normalizedReaction() {
74 return Emoticons.normalizeToVS16(this.reaction);
75 }
76
77 public static String toString(final Collection<Reaction> reactions) {
78 return (reactions == null || reactions.isEmpty()) ? null : GSON.toJson(reactions);
79 }
80
81 public static Collection<Reaction> fromString(final String asString) {
82 if (Strings.isNullOrEmpty(asString)) {
83 return Collections.emptyList();
84 }
85 try {
86 return GSON.fromJson(asString, new TypeToken<List<Reaction>>() {}.getType());
87 } catch (final IllegalArgumentException | JsonSyntaxException e) {
88 Log.e(Config.LOGTAG, "could not restore reactions", e);
89 return Collections.emptyList();
90 }
91 }
92
93 public static Collection<Reaction> withOccupantId(
94 final Collection<Reaction> existing,
95 final Collection<String> reactions,
96 final boolean received,
97 final Jid from,
98 final Jid trueJid,
99 final String occupantId) {
100 final ImmutableList.Builder<Reaction> builder = new ImmutableList.Builder<>();
101 builder.addAll(Collections2.filter(existing, e -> !occupantId.equals(e.occupantId)));
102 builder.addAll(
103 Collections2.transform(
104 reactions, r -> new Reaction(r, received, from, trueJid, occupantId)));
105 return builder.build();
106 }
107
108 @NonNull
109 @Override
110 public String toString() {
111 return MoreObjects.toStringHelper(this)
112 .add("reaction", reaction)
113 .add("received", received)
114 .add("from", from)
115 .add("trueJid", trueJid)
116 .add("occupantId", occupantId)
117 .toString();
118 }
119
120 public static Collection<Reaction> withFrom(
121 final Collection<Reaction> existing,
122 final Collection<String> reactions,
123 final boolean received,
124 final Jid from) {
125 final ImmutableList.Builder<Reaction> builder = new ImmutableList.Builder<>();
126 builder.addAll(
127 Collections2.filter(existing, e -> !from.asBareJid().equals(e.from.asBareJid())));
128 builder.addAll(
129 Collections2.transform(
130 reactions, r -> new Reaction(r, received, from, null, null)));
131 return builder.build();
132 }
133
134 private static class JidTypeAdapter extends TypeAdapter<Jid> {
135 @Override
136 public void write(final JsonWriter out, final Jid value) throws IOException {
137 if (value == null) {
138 out.nullValue();
139 } else {
140 out.value(value.toEscapedString());
141 }
142 }
143
144 @Override
145 public Jid read(final JsonReader in) throws IOException {
146 if (in.peek() == JsonToken.NULL) {
147 in.nextNull();
148 return null;
149 } else if (in.peek() == JsonToken.STRING) {
150 final String value = in.nextString();
151 return Jid.ofEscaped(value);
152 }
153 throw new IOException("Unexpected token");
154 }
155 }
156
157 public static Aggregated aggregated(final Collection<Reaction> reactions) {
158 final Map<String, Integer> aggregatedReactions =
159 Maps.transformValues(
160 Multimaps.index(reactions, Reaction::normalizedReaction).asMap(),
161 Collection::size);
162 final List<Map.Entry<String, Integer>> sortedList =
163 Ordering.from(
164 Comparator.comparingInt(
165 (Map.Entry<String, Integer> o) -> o.getValue()))
166 .reverse()
167 .immutableSortedCopy(aggregatedReactions.entrySet());
168 return new Aggregated(
169 sortedList,
170 ImmutableSet.copyOf(
171 Collections2.transform(
172 Collections2.filter(reactions, r -> !r.received),
173 Reaction::normalizedReaction)));
174 }
175
176 public static final class Aggregated {
177
178 public final List<Map.Entry<String, Integer>> reactions;
179 public final Set<String> ourReactions;
180
181 private Aggregated(
182 final List<Map.Entry<String, Integer>> reactions, Set<String> ourReactions) {
183 this.reactions = reactions;
184 this.ourReactions = ourReactions;
185 }
186 }
187}