Reply with a single emoji is a reaction

Stephen Paul Weber created

Change summary

src/main/java/eu/siacs/conversations/entities/Reaction.java    | 29 ++
src/main/java/eu/siacs/conversations/parser/MessageParser.java | 41 +++
2 files changed, 66 insertions(+), 4 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/entities/Reaction.java 🔗

@@ -82,6 +82,21 @@ public class Reaction {
         }
     }
 
+    public static Collection<Reaction> append(
+            final Collection<Reaction> existing,
+            final Collection<String> reactions,
+            final boolean received,
+            final Jid from,
+            final Jid trueJid,
+            final String occupantId) {
+        final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
+        builder.addAll(existing);
+        builder.addAll(
+                Collections2.transform(
+                        reactions, r -> new Reaction(r, received, from, trueJid, occupantId)));
+        return builder.build();
+    }
+
     public static Collection<Reaction> withOccupantId(
             final Collection<Reaction> existing,
             final Collection<String> reactions,
@@ -89,7 +104,7 @@ public class Reaction {
             final Jid from,
             final Jid trueJid,
             final String occupantId) {
-        final ImmutableList.Builder<Reaction> builder = new ImmutableList.Builder<>();
+        final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
         builder.addAll(Collections2.filter(existing, e -> !occupantId.equals(e.occupantId)));
         builder.addAll(
                 Collections2.transform(
@@ -109,12 +124,22 @@ public class Reaction {
                 .toString();
     }
 
+    public int hashCode() {
+        return toString().hashCode();
+    }
+
+    public boolean equals(Object o) {
+        if (o == null) return false;
+        if (!(o instanceof Reaction)) return false;
+        return toString().equals(o.toString());
+    }
+
     public static Collection<Reaction> withFrom(
             final Collection<Reaction> existing,
             final Collection<String> reactions,
             final boolean received,
             final Jid from) {
-        final ImmutableList.Builder<Reaction> builder = new ImmutableList.Builder<>();
+        final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
         builder.addAll(
                 Collections2.filter(existing, e -> !from.asBareJid().equals(e.from.asBareJid())));
         builder.addAll(

src/main/java/eu/siacs/conversations/parser/MessageParser.java 🔗

@@ -51,6 +51,7 @@ import eu.siacs.conversations.services.MessageArchiveService;
 import eu.siacs.conversations.services.QuickConversationsService;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.utils.CryptoHelper;
+import eu.siacs.conversations.utils.Emoticons;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.LocalizedContent;
 import eu.siacs.conversations.xml.Namespace;
@@ -466,7 +467,6 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
         if (timestamp == null) {
             timestamp = AbstractParser.parseTimestamp(original, AbstractParser.parseTimestamp(packet));
         }
-        final Reactions reactions = packet.getExtension(Reactions.class);
         final Element mucUserElement = packet.findChild("x", Namespace.MUC_USER);
         final String pgpEncrypted = packet.findChildContent("x", "jabber:x:encrypted");
         Element replaceElement = packet.findChild("replace", "urn:xmpp:message-correct:0");
@@ -502,6 +502,33 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
         }
         LocalizedContent body = packet.getBody();
 
+        var appendReactions = false;
+        var reactions = packet.getExtension(Reactions.class);
+        final var reply = packet.findChild("reply", "urn:xmpp:reply:0");
+        if (reactions == null && reply != null && reply.getAttribute("id") != null && body != null) {
+            StringBuilder bodyB = new StringBuilder(body.content);
+
+            for (Element el : packet.getChildren()) {
+                if ("fallback".equals(el.getName()) && "urn:xmpp:fallback:0".equals(el.getNamespace()) && "urn:xmpp:reply:0".equals(el.getAttribute("for"))) {
+                    for (final var span : el.getChildren()) {
+                        if (!span.getName().equals("body") && !span.getNamespace().equals("urn:xmpp:fallback:0")) continue;
+                        if (span.getAttribute("start") == null || span.getAttribute("end") == null) {
+                            bodyB.setLength(0);
+                        } else {
+                            bodyB.delete(bodyB.offsetByCodePoints(0, parseInt(span.getAttribute("start"))), bodyB.offsetByCodePoints(0, parseInt(span.getAttribute("end"))));
+                        }
+                    }
+                }
+            }
+
+            final var emojiMaybe = bodyB.toString().replaceAll("\\s", "");
+            if (Emoticons.isEmoji(emojiMaybe)) {
+                appendReactions = true;
+                reactions = im.conversations.android.xmpp.model.reactions.Reactions.to(reply.getAttribute("id"));
+                reactions.addExtension(new im.conversations.android.xmpp.model.reactions.Reaction(emojiMaybe));
+            }
+        }
+
         final Element axolotlEncrypted = packet.findChildEnsureSingle(XmppAxolotlMessage.CONTAINERTAG, AxolotlService.PEP_PREFIX);
         int status;
         final Jid counterpart;
@@ -609,7 +636,7 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
             }
         }
 
-        if ((body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || !attachments.isEmpty() || html != null || (packet.hasChild("subject") && packet.hasChild("thread"))) && !isMucStatusMessage) {
+        if (reactions == null && (body != null || pgpEncrypted != null || (axolotlEncrypted != null && axolotlEncrypted.hasChild("payload")) || !attachments.isEmpty() || html != null || (packet.hasChild("subject") && packet.hasChild("thread"))) && !isMucStatusMessage) {
             final Conversation conversation = mXmppConnectionService.findOrCreateConversation(account, counterpart.asBareJid(), conversationIsProbablyMuc, false, query, false);
             final boolean conversationMultiMode = conversation.getMode() == Conversation.MODE_MULTI;
 
@@ -1299,6 +1326,7 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
                     final boolean isReceived = !mucOptions.isSelf(counterpart);
                     if (occupantId != null && message != null) {
                         final var combinedReactions =
+                            appendReactions ? Reaction.append(message.getReactions(), reactions.getReactions(), isReceived, counterpart, null, occupantId) :
                                 Reaction.withOccupantId(
                                         message.getReactions(),
                                         reactions.getReactions(),
@@ -1325,6 +1353,7 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
                     packet.fromAccount(account);
                     if (message != null) {
                         final var combinedReactions =
+                            appendReactions ? Reaction.append(message.getReactions(), reactions.getReactions(), isReceived, reactionFrom, null, null) :
                                 Reaction.withFrom(
                                         message.getReactions(),
                                         reactions.getReactions(),
@@ -1469,4 +1498,12 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
             return true;
         }
     }
+
+    private static int parseInt(String value) {
+        try {
+            return Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+            return 0;
+        }
+    }
 }