Reaction.java

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