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