Better support for correcting replies/reactions

Stephen Paul Weber created

Change summary

src/main/java/eu/siacs/conversations/entities/Message.java               | 67 
src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java    | 22 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |  4 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java        | 17 
4 files changed, 90 insertions(+), 20 deletions(-)

Detailed changes

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

@@ -406,39 +406,63 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
     }
 
     public Message reply() {
-        Message m = new Message(conversation, QuoteHelper.quote(MessageUtils.prepareQuote(this)) + "\n", ENCRYPTION_NONE);
+        Message m = new Message(conversation, "", ENCRYPTION_NONE);
         m.setThread(getThread());
-        final String replyId = replyId();
-        if (replyId == null) return m;
 
-        m.addPayload(
+        m.updateReplyTo(this, null);
+        return m;
+    }
+
+    public void clearReplyReact() {
+        this.payloads.remove(getReactions());
+        this.payloads.remove(getReply());
+        clearFallbacks("urn:xmpp:reply:0", "urn:xmpp:reactions:0");
+    }
+
+    public void updateReplyTo(final Message replyTo, Spanned body) {
+        clearReplyReact();
+
+        if (body == null) body = new SpannableStringBuilder(getBody(true));
+        setBody(QuoteHelper.quote(MessageUtils.prepareQuote(replyTo)) + "\n");
+
+        final String replyId = replyTo.replyId();
+        if (replyId == null) return;
+
+        addPayload(
             new Element("reply", "urn:xmpp:reply:0")
-                .setAttribute("to", getCounterpart())
-                .setAttribute("id", replyId())
+                .setAttribute("to", replyTo.getCounterpart())
+                .setAttribute("id", replyId)
         );
         final Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reply:0");
         fallback.addChild("body", "urn:xmpp:fallback:0")
                 .setAttribute("start", "0")
-                .setAttribute("end", "" + m.body.codePointCount(0, m.body.length()));
-        m.addPayload(fallback);
-        return m;
+                .setAttribute("end", "" + this.body.codePointCount(0, this.body.length()));
+        addPayload(fallback);
+
+        appendBody(body);
     }
 
     public Message react(String emoji) {
-        Set<String> emojis = new HashSet<>();
-        if (conversation instanceof Conversation) emojis = ((Conversation) conversation).findReactionsTo(replyId(), null);
+        final var m = reply();
+        m.updateReaction(this, emoji);
+        return m;
+    }
+
+    public void updateReaction(final Message reactTo, String emoji) {
+         Set<String> emojis = new HashSet<>();
+        if (conversation instanceof Conversation) emojis = ((Conversation) conversation).findReactionsTo(reactTo.replyId(), null);
+        emojis.remove(getBody(true));
         emojis.add(emoji);
-        final Message m = reply();
-        m.appendBody(emoji);
+
+        updateReplyTo(reactTo, new SpannableStringBuilder(emoji));
         final Element fallback = new Element("fallback", "urn:xmpp:fallback:0").setAttribute("for", "urn:xmpp:reactions:0");
         fallback.addChild("body", "urn:xmpp:fallback:0");
-        m.addPayload(fallback);
+        addPayload(fallback);
         final Element reactions = new Element("reactions", "urn:xmpp:reactions:0").setAttribute("id", replyId());
         for (String oneEmoji : emojis) {
             reactions.addChild("reaction", "urn:xmpp:reactions:0").setContent(oneEmoji);
         }
-        m.addPayload(reactions);
-        return m;
+        addPayload(reactions);
     }
 
     public void setReactions(Element reactions) {
@@ -553,9 +577,16 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
     }
 
     public String getBody() {
+        return getBody(false);
+    }
+
+    public String getBody(final boolean removeQuoteFallbacks) {
         if (body == null) return "";
 
-        Pair<StringBuilder, Boolean> result = bodyMinusFallbacks("http://jabber.org/protocol/address", Namespace.OOB);
+        Pair<StringBuilder, Boolean> result =
+            removeQuoteFallbacks
+            ? bodyMinusFallbacks("http://jabber.org/protocol/address", Namespace.OOB, "urn:xmpp:reply:0")
+            : bodyMinusFallbacks("http://jabber.org/protocol/address", Namespace.OOB);
         StringBuilder body = result.first;
 
         final String aesgcm = MessageUtils.aesgcmDownloadable(body.toString());
@@ -579,7 +610,7 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
         if (html != null) return html;
         html = new Element("html", "http://jabber.org/protocol/xhtml-im");
         Element body = html.addChild("body", "http://www.w3.org/1999/xhtml");
-        SpannedToXHTML.append(body, new SpannableStringBuilder(getBody()));
+        SpannedToXHTML.append(body, new SpannableStringBuilder(getBody(true)));
         addPayload(html);
         return body;
     }

src/main/java/eu/siacs/conversations/persistance/DatabaseBackend.java 🔗

@@ -1097,6 +1097,28 @@ public class DatabaseBackend extends SQLiteOpenHelper {
         return getMessages(conversations, limit, -1);
     }
 
+    public Message getMessageFuzzyId(Conversation conversation, String id) {
+        ArrayList<Message> list = new ArrayList<>();
+        SQLiteDatabase db = this.getReadableDatabase();
+        Cursor cursor;
+        cursor = db.rawQuery(
+            "SELECT * FROM " + Message.TABLENAME + " " +
+            "LEFT JOIN cheogram." + Message.TABLENAME +
+            "  USING (" + Message.UUID + ")" +
+            "WHERE " + Message.UUID + "=? OR " + Message.SERVER_MSG_ID + " =? OR " + Message.REMOTE_MSG_ID + " =?",
+            new String[]{id,id,id}
+        );
+        while (cursor.moveToNext()) {
+            try {
+                return Message.fromCursor(cursor, conversation);
+            } catch (Exception e) {
+                Log.e(Config.LOGTAG, "unable to restore message");
+            }
+        }
+        cursor.close();
+        return null;
+    }
+
     public ArrayList<Message> getMessages(Conversation conversation, int limit, long timestamp) {
         ArrayList<Message> list = new ArrayList<>();
         SQLiteDatabase db = this.getReadableDatabase();

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

@@ -671,6 +671,10 @@ public class XmppConnectionService extends Service {
         return this.databaseBackend.getMessage(conversation, uuid);
     }
 
+    public Message getMessageFuzzyId(Conversation conversation, String id) {
+        return this.databaseBackend.getMessageFuzzyId(conversation, id);
+    }
+
     public void insertWebxdcUpdate(final WebxdcUpdate update) {
         this.databaseBackend.insertWebxdcUpdate(update);
     }

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

@@ -1000,9 +1000,18 @@ public class ConversationFragment extends XmppFragment
             Message.configurePrivateMessage(message);
         } else {
             message = conversation.getCorrectingMessage();
-            message.setBody(hasSubject && body.length() == 0 ? null : body);
             if (hasSubject) message.setSubject(binding.textinputSubject.getText().toString());
             message.setThread(conversation.getThread());
+            if (conversation.getReplyTo() != null) {
+                if (Emoticons.isEmoji(body.toString().replaceAll("\\s", ""))) {
+                    message.updateReaction(conversation.getReplyTo(), body.toString().replaceAll("\\s", ""));
+                } else {
+                    message.updateReplyTo(conversation.getReplyTo(), body);
+                }
+            } else {
+                message.clearReplyReact();
+                message.setBody(hasSubject && body.length() == 0 ? null : body);
+            }
             if (message.getStatus() == Message.STATUS_WAITING) {
                 if (sendAt != null) message.setTime(sendAt);
                 activity.xmppConnectionService.updateMessage(message);
@@ -2933,11 +2942,15 @@ public class ConversationFragment extends XmppFragment
         final Editable editable = binding.textinput.getText();
         this.conversation.setDraftMessage(editable.toString());
         this.binding.textinput.setText("");
-        this.binding.textinput.append(message.getBody());
+        this.binding.textinput.append(message.getBody(true));
         if (message.getSubject() != null && message.getSubject().length() > 0) {
             this.binding.textinputSubject.setText(message.getSubject());
             this.binding.textinputSubject.setVisibility(View.VISIBLE);
         }
+        final var reply = message.getReply();
+        if (reply != null) {
+            setupReply(activity.xmppConnectionService.getMessageFuzzyId(conversation, reply.getAttribute("id")));
+        }
     }
 
     private void highlightInConference(String nick) {