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