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.common.reflect.TypeToken;
16import com.google.gson.Gson;
17import com.google.gson.GsonBuilder;
18import com.google.gson.JsonSyntaxException;
19import com.google.gson.TypeAdapter;
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.ArrayList;
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 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 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.toEscapedString());
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.ofEscaped(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, r -> r.reaction).asMap(), Collection::size);
157 final List<Map.Entry<String, Integer>> sortedList =
158 Ordering.from(
159 Comparator.comparingInt(
160 (Map.Entry<String, Integer> o) -> o.getValue()))
161 .reverse()
162 .immutableSortedCopy(aggregatedReactions.entrySet());
163 return new Aggregated(
164 sortedList,
165 ImmutableSet.copyOf(
166 Collections2.transform(
167 Collections2.filter(reactions, r -> !r.received),
168 r -> r.reaction)));
169 }
170
171 public static final class Aggregated {
172
173 public final List<Map.Entry<String, Integer>> reactions;
174 public final Set<String> ourReactions;
175
176 private Aggregated(
177 final List<Map.Entry<String, Integer>> reactions, Set<String> ourReactions) {
178 this.reactions = reactions;
179 this.ourReactions = ourReactions;
180 }
181 }
182}