1package eu.siacs.conversations.entities;
  2
  3import android.util.Log;
  4import androidx.annotation.NonNull;
  5
  6import com.cheogram.android.EmojiSearch;
  7import com.cheogram.android.GetThumbnailForCid;
  8
  9import com.google.common.base.MoreObjects;
 10import com.google.common.base.Strings;
 11import com.google.common.collect.Collections2;
 12import com.google.common.collect.ImmutableList;
 13import com.google.common.collect.ImmutableSet;
 14import com.google.common.collect.Maps;
 15import com.google.common.collect.Multimaps;
 16import com.google.common.collect.Ordering;
 17import com.google.gson.Gson;
 18import com.google.gson.GsonBuilder;
 19import com.google.gson.JsonSyntaxException;
 20import com.google.gson.TypeAdapter;
 21import com.google.gson.reflect.TypeToken;
 22import com.google.gson.stream.JsonReader;
 23import com.google.gson.stream.JsonToken;
 24import com.google.gson.stream.JsonWriter;
 25
 26import io.ipfs.cid.Cid;
 27
 28import eu.siacs.conversations.Config;
 29import eu.siacs.conversations.utils.Emoticons;
 30import eu.siacs.conversations.xmpp.Jid;
 31import java.io.IOException;
 32import java.util.Arrays;
 33import java.util.Collection;
 34import java.util.Collections;
 35import java.util.Comparator;
 36import java.util.List;
 37import java.util.Map;
 38import java.util.Set;
 39import java.util.function.Function;
 40
 41public class Reaction {
 42
 43    public static final List<String> SUGGESTIONS =
 44            Arrays.asList(
 45                    "\u2764\uFE0F",
 46                    "\uD83D\uDC4D",
 47                    "\uD83D\uDC4E",
 48                    "\uD83D\uDE02",
 49                    "\u2757",
 50                    "\u2753");
 51
 52    private static final Gson GSON;
 53
 54    static {
 55        GSON = new GsonBuilder().registerTypeAdapter(Jid.class, new JidTypeAdapter()).registerTypeAdapter(Cid.class, new CidTypeAdapter()).create();
 56    }
 57
 58    public final String reaction;
 59    public final boolean received;
 60    public final Jid from;
 61    public final Jid trueJid;
 62    public final String occupantId;
 63    public final Cid cid;
 64    public final String envelopeId;
 65
 66    public Reaction(
 67            final String reaction,
 68            final Cid cid,
 69            boolean received,
 70            final Jid from,
 71            final Jid trueJid,
 72            final String occupantId,
 73            final String envelopeId) {
 74        this.reaction = reaction;
 75        this.cid = cid;
 76        this.received = received;
 77        this.from = from;
 78        this.trueJid = trueJid;
 79        this.occupantId = occupantId;
 80        this.envelopeId = envelopeId;
 81    }
 82
 83    public String normalizedReaction() {
 84        return Emoticons.normalizeToVS16(this.reaction);
 85    }
 86
 87    public static String toString(final Collection<Reaction> reactions) {
 88        return (reactions == null || reactions.isEmpty()) ? null : GSON.toJson(reactions);
 89    }
 90
 91    public static Collection<Reaction> fromString(final String asString) {
 92        if (Strings.isNullOrEmpty(asString)) {
 93            return Collections.emptyList();
 94        }
 95        try {
 96            return GSON.fromJson(asString, new TypeToken<List<Reaction>>() {}.getType());
 97        } catch (final IllegalArgumentException | JsonSyntaxException e) {
 98            Log.e(Config.LOGTAG, "could not restore reactions", e);
 99            return Collections.emptyList();
100        }
101    }
102
103    public static Collection<Reaction> withMine(
104            final Collection<Reaction> existing,
105            final Collection<String> reactions,
106            final boolean received,
107            final Jid from,
108            final Jid trueJid,
109            final String occupantId,
110            final String envelopeId) {
111         final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
112        builder.addAll(Collections2.filter(existing, e -> e.received));
113        builder.addAll(
114                Collections2.transform(
115                        reactions, r -> new Reaction(r, null, received, from, trueJid, occupantId, envelopeId)));
116        return builder.build();
117    }
118
119    public static Collection<Reaction> withOccupantId(
120            final Collection<Reaction> existing,
121            final Collection<String> reactions,
122            final boolean received,
123            final Jid from,
124            final Jid trueJid,
125            final String occupantId,
126            final String envelopeId) {
127        final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
128        builder.addAll(Collections2.filter(existing, e -> !occupantId.equals(e.occupantId)));
129        builder.addAll(
130                Collections2.transform(
131                        reactions, r -> new Reaction(r, null, received, from, trueJid, occupantId, envelopeId)));
132        return builder.build();
133    }
134
135    @NonNull
136    @Override
137    public String toString() {
138        return MoreObjects.toStringHelper(this)
139                .add("reaction", cid == null ? reaction : null)
140                .add("cid", cid)
141                .add("received", received)
142                .add("from", from)
143                .add("trueJid", trueJid)
144                .add("occupantId", occupantId)
145                .toString();
146    }
147
148    public int hashCode() {
149        return toString().hashCode();
150    }
151
152    public boolean equals(Object o) {
153        if (o == null) return false;
154        if (!(o instanceof Reaction)) return false;
155        return toString().equals(o.toString());
156    }
157
158    public static Collection<Reaction> withFrom(
159            final Collection<Reaction> existing,
160            final Collection<String> reactions,
161            final boolean received,
162            final Jid from,
163            final String envelopeId) {
164        final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
165        builder.addAll(
166                Collections2.filter(existing, e -> !from.asBareJid().equals(e.from.asBareJid())));
167        builder.addAll(
168                Collections2.transform(
169                        reactions, r -> new Reaction(r, null, received, from, null, null, envelopeId)));
170        return builder.build();
171    }
172
173    private static class JidTypeAdapter extends TypeAdapter<Jid> {
174        @Override
175        public void write(final JsonWriter out, final Jid value) throws IOException {
176            if (value == null) {
177                out.nullValue();
178            } else {
179                out.value(value.toString());
180            }
181        }
182
183        @Override
184        public Jid read(final JsonReader in) throws IOException {
185            if (in.peek() == JsonToken.NULL) {
186                in.nextNull();
187                return null;
188            } else if (in.peek() == JsonToken.STRING) {
189                final String value = in.nextString();
190                return Jid.of(value);
191            }
192            throw new IOException("Unexpected token");
193        }
194    }
195
196    private static class CidTypeAdapter extends TypeAdapter<Cid> {
197        @Override
198        public void write(final JsonWriter out, final Cid value) throws IOException {
199            if (value == null) {
200                out.nullValue();
201            } else {
202                out.value(value.toString());
203            }
204        }
205
206        @Override
207        public Cid read(final JsonReader in) throws IOException {
208            if (in.peek() == JsonToken.NULL) {
209                in.nextNull();
210                return null;
211            } else if (in.peek() == JsonToken.STRING) {
212                final String value = in.nextString();
213                return Cid.decode(value);
214            }
215            throw new IOException("Unexpected token");
216        }
217    }
218
219    public static Aggregated aggregated(final Collection<Reaction> reactions) {
220        return aggregated(reactions, (r) -> null);
221    }
222
223    public static Aggregated aggregated(final Collection<Reaction> reactions, Function<Reaction, GetThumbnailForCid> thumbnailer) {
224        final Map<EmojiSearch.Emoji, Collection<Reaction>> aggregatedReactions =
225                        Multimaps.index(reactions, r -> r.cid == null ? new EmojiSearch.Emoji(r.reaction, 0) : new EmojiSearch.CustomEmoji(r.reaction, r.cid.toString(), thumbnailer.apply(r).getThumbnail(r.cid), null)).asMap();
226        final List<Map.Entry<EmojiSearch.Emoji, Collection<Reaction>>> sortedList =
227                Ordering.from(
228                                Comparator.comparingInt(
229                                        (Map.Entry<EmojiSearch.Emoji, Collection<Reaction>> o) -> o.getValue().size()))
230                        .reverse()
231                        .immutableSortedCopy(aggregatedReactions.entrySet());
232        return new Aggregated(
233                sortedList,
234                ImmutableSet.copyOf(
235                        Collections2.transform(
236                                Collections2.filter(reactions, r -> r.cid == null && !r.received),
237                                Reaction::normalizedReaction)));
238    }
239
240    public static final class Aggregated {
241
242        public final List<Map.Entry<EmojiSearch.Emoji, Collection<Reaction>>> reactions;
243        public final Set<String> ourReactions;
244
245        private Aggregated(
246                final List<Map.Entry<EmojiSearch.Emoji, Collection<Reaction>>> reactions, Set<String> ourReactions) {
247            this.reactions = reactions;
248            this.ourReactions = ourReactions;
249        }
250    }
251}