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.common.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 "\uD83D\uDE2E",
43 "\uD83D\uDE22");
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> withOccupantId(
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 ImmutableList.Builder<Reaction> builder = new ImmutableList.Builder<>();
93 builder.addAll(Collections2.filter(existing, e -> !occupantId.equals(e.occupantId)));
94 builder.addAll(
95 Collections2.transform(
96 reactions, r -> new Reaction(r, received, from, trueJid, occupantId)));
97 return builder.build();
98 }
99
100 @NonNull
101 @Override
102 public String toString() {
103 return MoreObjects.toStringHelper(this)
104 .add("reaction", reaction)
105 .add("received", received)
106 .add("from", from)
107 .add("trueJid", trueJid)
108 .add("occupantId", occupantId)
109 .toString();
110 }
111
112 public static Collection<Reaction> withFrom(
113 final Collection<Reaction> existing,
114 final Collection<String> reactions,
115 final boolean received,
116 final Jid from) {
117 final ImmutableList.Builder<Reaction> builder = new ImmutableList.Builder<>();
118 builder.addAll(
119 Collections2.filter(existing, e -> !from.asBareJid().equals(e.from.asBareJid())));
120 builder.addAll(
121 Collections2.transform(
122 reactions, r -> new Reaction(r, received, from, null, null)));
123 return builder.build();
124 }
125
126 private static class JidTypeAdapter extends TypeAdapter<Jid> {
127 @Override
128 public void write(final JsonWriter out, final Jid value) throws IOException {
129 if (value == null) {
130 out.nullValue();
131 } else {
132 out.value(value.toEscapedString());
133 }
134 }
135
136 @Override
137 public Jid read(final JsonReader in) throws IOException {
138 if (in.peek() == JsonToken.NULL) {
139 in.nextNull();
140 return null;
141 } else if (in.peek() == JsonToken.STRING) {
142 final String value = in.nextString();
143 return Jid.ofEscaped(value);
144 }
145 throw new IOException("Unexpected token");
146 }
147 }
148
149 public static Aggregated aggregated(final Collection<Reaction> reactions) {
150 final Map<String, Integer> aggregatedReactions =
151 Maps.transformValues(
152 Multimaps.index(reactions, r -> r.reaction).asMap(), Collection::size);
153 final List<Map.Entry<String, Integer>> sortedList =
154 Ordering.from(
155 Comparator.comparingInt(
156 (Map.Entry<String, Integer> o) -> o.getValue()))
157 .reverse()
158 .immutableSortedCopy(aggregatedReactions.entrySet());
159 return new Aggregated(
160 sortedList,
161 ImmutableSet.copyOf(
162 Collections2.transform(
163 Collections2.filter(reactions, r -> !r.received),
164 r -> r.reaction)));
165 }
166
167 public static final class Aggregated {
168
169 public final List<Map.Entry<String, Integer>> reactions;
170 public final Set<String> ourReactions;
171
172 private Aggregated(
173 final List<Map.Entry<String, Integer>> reactions, Set<String> ourReactions) {
174 this.reactions = reactions;
175 this.ourReactions = ourReactions;
176 }
177 }
178}