Allow removing custom reaction tapping the chip

Stephen Paul Weber created

Change summary

src/main/java/eu/siacs/conversations/entities/Conversation.java          | 22 
src/main/java/eu/siacs/conversations/entities/Reaction.java              | 15 
src/main/java/eu/siacs/conversations/parser/MessageParser.java           |  6 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |  6 
src/main/java/eu/siacs/conversations/ui/BindingAdapters.java             | 18 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java      | 22 
6 files changed, 72 insertions(+), 17 deletions(-)

Detailed changes

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

@@ -297,6 +297,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             for (int i = messages.size() - 1; i >= 0; --i) {
                 final Message message = messages.get(i);
                 if (message.getSubject() != null && !message.isOOb() && (message.getRawBody() == null || message.getRawBody().length() == 0)) continue;
+                if ((message.getRawBody() == null || "".equals(message.getRawBody()) || " ".equals(message.getRawBody())) && message.getReply() != null && message.edited() && message.getHtml() != null) continue;
                 if (asReaction(message) != null) continue;
                 if (message.isRead()) {
                     return first;
@@ -313,6 +314,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
         synchronized (this.messages) {
             for (final Message message : Lists.reverse(this.messages)) {
                 if (message.getSubject() != null && !message.isOOb() && (message.getRawBody() == null || message.getRawBody().length() == 0)) continue;
+                if ((message.getRawBody() == null || "".equals(message.getRawBody()) || " ".equals(message.getRawBody())) && message.getReply() != null && message.edited() && message.getHtml() != null) continue;
                 if (asReaction(message) != null) continue;
                 if (message.getStatus() == Message.STATUS_RECEIVED) {
                     final String serverMsgId = message.getServerMsgId();
@@ -714,6 +716,12 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                     thread.first = m;
                 }
             }
+
+            if ((m.getRawBody() == null || "".equals(m.getRawBody()) || " ".equals(m.getRawBody())) && m.getReply() != null && m.edited() && m.getHtml() != null) {
+                iterator.remove();
+                continue;
+            }
+
             final var asReaction = asReaction(m);
             if (asReaction != null) {
                 reactions.put(asReaction.first, asReaction.second);
@@ -734,9 +742,16 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
     protected Pair<String, Reaction> asReaction(Message m) {
         final var reply = m.getReply();
         if (reply != null && reply.getAttribute("id") != null) {
+            final String envelopeId;
+            if (m.isCarbon() || m.getStatus() == Message.STATUS_RECEIVED) {
+                envelopeId = m.getRemoteMsgId();
+            } else {
+                envelopeId = m.getUuid();
+            }
+
             final var body = m.getBody(true).toString().replaceAll("\\s", "");
             if (Emoticons.isEmoji(body)) {
-                return new Pair<>(reply.getAttribute("id"), new Reaction(body, null, m.getStatus() <= Message.STATUS_RECEIVED, m.getCounterpart(), m.getTrueCounterpart(), m.getOccupantId()));
+                return new Pair<>(reply.getAttribute("id"), new Reaction(body, null, m.getStatus() <= Message.STATUS_RECEIVED, m.getCounterpart(), m.getTrueCounterpart(), m.getOccupantId(), envelopeId));
             } else {
                 final var html = m.getHtml();
                 if (html == null) return null;
@@ -758,7 +773,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
                     }
                     if (source != null && source.length() > 0 && source.substring(0, 4).equals("cid:")) {
                         final Cid cid = BobTransfer.cid(Uri.parse(source));
-                        return new Pair<>(reply.getAttribute("id"), new Reaction(shortcode, cid, m.getStatus() <= Message.STATUS_RECEIVED, m.getCounterpart(), m.getTrueCounterpart(), m.getOccupantId()));
+                        return new Pair<>(reply.getAttribute("id"), new Reaction(shortcode, cid, m.getStatus() <= Message.STATUS_RECEIVED, m.getCounterpart(), m.getTrueCounterpart(), m.getOccupantId(), envelopeId));
                     }
                 }
             }
@@ -941,6 +956,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             for(final Message message : Lists.reverse(this.messages)) {
                 if (message.getSubject() != null && !message.isOOb() && (message.getRawBody() == null || message.getRawBody().length() == 0)) continue;
                 if (asReaction(message) != null) continue;
+                if ((message.getRawBody() == null || "".equals(message.getRawBody()) || " ".equals(message.getRawBody())) && message.getReply() != null && message.edited() && message.getHtml() != null) continue;
                 return message;
             }
 
@@ -1411,6 +1427,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             for(final Message message : Lists.reverse(this.messages)) {
                 if (message.getSubject() != null && !message.isOOb() && (message.getRawBody() == null || message.getRawBody().length() == 0)) continue;
                 if (asReaction(message) != null) continue;
+                if ((message.getRawBody() == null || "".equals(message.getRawBody()) || " ".equals(message.getRawBody())) && message.getReply() != null && message.edited() && message.getHtml() != null) continue;
                 final boolean muted = xmppConnectionService != null && message.getStatus() == Message.STATUS_RECEIVED && getMode() == Conversation.MODE_MULTI && xmppConnectionService.isMucUserMuted(new MucOptions.User(null, getJid(), message.getOccupantId(), null, null));
                 if (muted) continue;
                 if (message.isRead()) {
@@ -1431,6 +1448,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
             for (Message message : messages) {
                 if (message.getSubject() != null && !message.isOOb() && (message.getRawBody() == null || message.getRawBody().length() == 0)) continue;
                 if (asReaction(message) != null) continue;
+                if ((message.getRawBody() == null || "".equals(message.getRawBody()) || " ".equals(message.getRawBody())) && message.getReply() != null && message.edited() && message.getHtml() != null) continue;
                 if (message.getStatus() == Message.STATUS_RECEIVED) {
                     ++count;
                 }

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

@@ -60,6 +60,7 @@ public class Reaction {
     public final Jid trueJid;
     public final String occupantId;
     public final Cid cid;
+    public final String envelopeId;
 
     public Reaction(
             final String reaction,
@@ -67,13 +68,15 @@ public class Reaction {
             boolean received,
             final Jid from,
             final Jid trueJid,
-            final String occupantId) {
+            final String occupantId,
+            final String envelopeId) {
         this.reaction = reaction;
         this.cid = cid;
         this.received = received;
         this.from = from;
         this.trueJid = trueJid;
         this.occupantId = occupantId;
+        this.envelopeId = envelopeId;
     }
 
     public static String toString(final Collection<Reaction> reactions) {
@@ -97,12 +100,13 @@ public class Reaction {
             final boolean received,
             final Jid from,
             final Jid trueJid,
-            final String occupantId) {
+            final String occupantId,
+            final String envelopeId) {
         final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
         builder.addAll(Collections2.filter(existing, e -> !occupantId.equals(e.occupantId)));
         builder.addAll(
                 Collections2.transform(
-                        reactions, r -> new Reaction(r, null, received, from, trueJid, occupantId)));
+                        reactions, r -> new Reaction(r, null, received, from, trueJid, occupantId, envelopeId)));
         return builder.build();
     }
 
@@ -133,13 +137,14 @@ public class Reaction {
             final Collection<Reaction> existing,
             final Collection<String> reactions,
             final boolean received,
-            final Jid from) {
+            final Jid from,
+            final String envelopeId) {
         final ImmutableSet.Builder<Reaction> builder = new ImmutableSet.Builder<>();
         builder.addAll(
                 Collections2.filter(existing, e -> !from.asBareJid().equals(e.from.asBareJid())));
         builder.addAll(
                 Collections2.transform(
-                        reactions, r -> new Reaction(r, null, received, from, null, null)));
+                        reactions, r -> new Reaction(r, null, received, from, null, null, envelopeId)));
         return builder.build();
     }
 

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

@@ -1306,7 +1306,8 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
                                         isReceived,
                                         counterpart,
                                         null,
-                                        occupantId);
+                                        occupantId,
+                                        message.getRemoteMsgId());
                         message.setReactions(combinedReactions);
                         mXmppConnectionService.updateMessage(message, false);
                     } else {
@@ -1330,7 +1331,8 @@ public class MessageParser extends AbstractParser implements Consumer<im.convers
                                         message.getReactions(),
                                         reactions.getReactions(),
                                         isReceived,
-                                        reactionFrom);
+                                        reactionFrom,
+                                        message.getRemoteMsgId());
                         message.setReactions(combinedReactions);
                         mXmppConnectionService.updateMessage(message, false);
                     }

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -5288,7 +5288,8 @@ public class XmppConnectionService extends Service {
                                 false,
                                 self.getFullJid(),
                                 conversation.getAccount().getJid(),
-                                occupantId);
+                                occupantId,
+                                null);
             } else {
                 if (message.isCarbon() || message.getStatus() == Message.STATUS_RECEIVED) {
                     reactToId = message.getRemoteMsgId();
@@ -5300,7 +5301,8 @@ public class XmppConnectionService extends Service {
                                 message.getReactions(),
                                 reactions,
                                 false,
-                                conversation.getAccount().getJid());
+                                conversation.getAccount().getJid(),
+                                null);
             }
             if (Strings.isNullOrEmpty(reactToId)) {
                 return false;

src/main/java/eu/siacs/conversations/ui/BindingAdapters.java 🔗

@@ -32,15 +32,16 @@ public class BindingAdapters {
             final Reaction.Aggregated reactions,
             final Consumer<Collection<String>> onModifiedReactions,
             final Consumer<EmojiSearch.CustomEmoji> onCustomReaction,
+            final Consumer<Reaction> onCustomReactionRemove,
             final Runnable addReaction) {
-        setReactions(chipGroup, conversation, reactions, true, onModifiedReactions, onCustomReaction, addReaction);
+        setReactions(chipGroup, conversation, reactions, true, onModifiedReactions, onCustomReaction, onCustomReactionRemove, addReaction);
     }
 
     public static void setReactionsOnSent(
             final ChipGroup chipGroup,
             final Reaction.Aggregated reactions,
             final Consumer<Collection<String>> onModifiedReactions) {
-        setReactions(chipGroup, null, reactions, false, onModifiedReactions, null, null);
+        setReactions(chipGroup, null, reactions, false, onModifiedReactions, null, null, null);
     }
 
     private static void setReactions(
@@ -50,6 +51,7 @@ public class BindingAdapters {
             final boolean onReceived,
             final Consumer<Collection<String>> onModifiedReactions,
             final Consumer<EmojiSearch.CustomEmoji> onCustomReaction,
+            final Consumer<Reaction> onCustomReactionRemove,
             final Runnable addReaction) {
         final var context = chipGroup.getContext();
         final var size = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 35, context.getResources().getDisplayMetrics());
@@ -71,9 +73,9 @@ public class BindingAdapters {
                 chip.setLayoutParams(layoutParams);
                 chip.setChipCornerRadius(corner);
                 emoji.setupChip(chip, count);
-                final boolean oneOfOurs = aggregated.ourReactions.contains(emoji.toString());
+                final var oneOfOurs = reaction.getValue().stream().filter(r -> !r.received).findFirst();
                 // received = surface; sent = surface high matches bubbles
-                if (oneOfOurs) {
+                if (oneOfOurs.isPresent()) {
                     chip.setChipBackgroundColor(
                             MaterialColors.getColorStateListOrNull(
                                     context,
@@ -89,12 +91,16 @@ public class BindingAdapters {
                 chip.setTextStartPadding(0.0f);
                 chip.setOnClickListener(
                         v -> {
-                            if (oneOfOurs) {
-                                onModifiedReactions.accept(
+                            if (oneOfOurs.isPresent()) {
+                                if (emoji instanceof EmojiSearch.CustomEmoji) {
+                                    onCustomReactionRemove.accept(oneOfOurs.get());
+                                } else {
+                                    onModifiedReactions.accept(
                                         ImmutableSet.copyOf(
                                                 Collections2.filter(
                                                         aggregated.ourReactions,
                                                         r -> !r.equals(emoji.toString()))));
+                                }
                             } else {
                                 if (emoji instanceof EmojiSearch.CustomEmoji) {
                                     onCustomReaction.accept((EmojiSearch.CustomEmoji) emoji);

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java 🔗

@@ -1524,6 +1524,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                     aggregatedReactions,
                     reactions -> sendReactions(message, reactions),
                     emoji -> sendCustomReaction(message, emoji),
+                    reaction -> removeCustomReaction(conversation, reaction),
                     () -> addReaction(message));
         } else if (type == SENT) {
             final var aggregatedReactions = conversation instanceof Conversation ? ((Conversation) conversation).aggregatedReactionsFor(message, reactionThumbnailer) : message.getAggregatedReactions();
@@ -1533,6 +1534,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
                     aggregatedReactions,
                     reactions -> sendReactions(message, reactions),
                     emoji -> sendCustomReaction(message, emoji),
+                    reaction -> removeCustomReaction(conversation, reaction),
                     () -> addReaction(message));
         }
 
@@ -1615,6 +1617,26 @@ public class MessageAdapter extends ArrayAdapter<Message> {
         new Thread(() -> activity.xmppConnectionService.sendMessage(message)).start();
     }
 
+    private void removeCustomReaction(final Conversational conversation, final Reaction reaction) {
+        if (!(conversation instanceof Conversation)) {
+            Toast.makeText(activity, R.string.could_not_add_reaction, Toast.LENGTH_LONG).show();
+            return;
+        }
+
+        final var message = new Message(conversation, " ", ((Conversation) conversation).getNextEncryption());
+        final var envelope = ((Conversation) conversation).findMessageWithUuidOrRemoteId(reaction.envelopeId);
+        if (envelope != null) {
+            ((Conversation) conversation).remove(envelope);
+            message.addPayload(envelope.getReply());
+            message.getOrMakeHtml();
+            message.putEdited(reaction.envelopeId, envelope.getServerMsgId());
+        } else {
+            message.putEdited(reaction.envelopeId, null);
+        }
+
+        new Thread(() -> activity.xmppConnectionService.sendMessage(message)).start();
+    }
+
     private void addReaction(final Message message) {
         activity.addReaction(message, reactions -> activity.xmppConnectionService.sendReactions(message,reactions));
     }