Merge branch 'reply'

Stephen Paul Weber created

* reply:
  UI for rich replies
  Reply to message instead of just quote
  Move quote code to quote helper

Change summary

src/cheogram/res/drawable/ic_reply_black.xml                        | 11 
src/cheogram/res/values/themes.xml                                  |  2 
src/main/java/eu/siacs/conversations/entities/Conversation.java     |  9 
src/main/java/eu/siacs/conversations/entities/Message.java          | 24 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java   | 29 
src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java |  2 
src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java       | 13 
src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java     |  9 
src/main/res/layout/fragment_conversation.xml                       | 40 
9 files changed, 125 insertions(+), 14 deletions(-)

Detailed changes

src/cheogram/res/drawable/ic_reply_black.xml ๐Ÿ”—

@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="24"
+    android:viewportHeight="24"
+    android:tint="?attr/colorControlNormal"
+    android:autoMirrored="true">
+  <path
+      android:fillColor="@android:color/white"
+      android:pathData="M10,9V5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z"/>
+</vector>

src/cheogram/res/values/themes.xml ๐Ÿ”—

@@ -108,7 +108,7 @@
         <item name="icon_save" type="reference">@drawable/ic_save_black_24dp</item>
         <item name="icon_group" type="reference">@drawable/ic_group_white_24dp</item>
         <item name="icon_new" type="reference">@drawable/ic_add_white_24dp</item>
-        <item name="icon_quote" type="reference">@drawable/ic_reply_white_24dp</item>
+        <item name="icon_quote" type="reference">@drawable/ic_reply_black</item>
         <item name="icon_refresh" type="reference">@drawable/ic_refresh_black_24dp</item>
         <item name="icon_new_attachment" type="reference">@drawable/ic_attach_file_white_24dp</item>
         <item name="icon_not_secure" type="reference">@drawable/ic_lock_open_white_24dp</item>

src/main/java/eu/siacs/conversations/entities/Conversation.java ๐Ÿ”—

@@ -170,6 +170,7 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
     protected Element thread = null;
     protected boolean lockThread = false;
     protected boolean userSelectedThread = false;
+    protected Message replyTo = null;
 
     public Conversation(final String name, final Account account, final Jid contactJid,
                         final int mode) {
@@ -671,6 +672,14 @@ public class Conversation extends AbstractEntity implements Blockable, Comparabl
         return this.userSelectedThread;
     }
 
+    public void setReplyTo(Message m) {
+        this.replyTo = m;
+    }
+
+    public Message getReplyTo() {
+        return this.replyTo;
+    }
+
     public boolean isRead() {
         synchronized (this.messages) {
             for(final Message message : Lists.reverse(this.messages)) {

src/main/java/eu/siacs/conversations/entities/Message.java ๐Ÿ”—

@@ -47,6 +47,7 @@ import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 import eu.siacs.conversations.http.URL;
 import eu.siacs.conversations.services.AvatarService;
 import eu.siacs.conversations.ui.util.PresenceSelector;
+import eu.siacs.conversations.ui.util.QuoteHelper;
 import eu.siacs.conversations.utils.CryptoHelper;
 import eu.siacs.conversations.utils.Emoticons;
 import eu.siacs.conversations.utils.GeoHelper;
@@ -379,6 +380,22 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
         return values;
     }
 
+    public Message reply() {
+        Message m = new Message(conversation, QuoteHelper.quote(getBody()) + "\n", ENCRYPTION_NONE);
+        m.setThread(getThread());
+        m.addPayload(
+            new Element("reply", "urn:xmpp:reply:0")
+                .setAttribute("to", getCounterpart())
+                .setAttribute("id", conversation.getMode() == Conversation.MODE_MULTI ? getServerMsgId() : getRemoteMsgId())
+        );
+        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.length());
+        m.addPayload(fallback);
+        return m;
+    }
+
     public String getConversationUuid() {
         return conversationUuid;
     }
@@ -450,6 +467,13 @@ public class Message extends AbstractEntity implements AvatarService.Avatarable
         this.treatAsDownloadable = null;
     }
 
+    public synchronized void appendBody(String append) {
+        this.body += append;
+        this.isGeoUri = null;
+        this.isEmojisOnly = null;
+        this.treatAsDownloadable = null;
+    }
+
     public String getSubject() {
         return subject;
     }

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java ๐Ÿ”—

@@ -30,6 +30,7 @@ import android.os.SystemClock;
 import android.preference.PreferenceManager;
 import android.provider.MediaStore;
 import android.text.Editable;
+import android.text.SpannableStringBuilder;
 import android.text.TextUtils;
 import android.util.Log;
 import android.view.ContextMenu;
@@ -893,7 +894,13 @@ public class ConversationFragment extends XmppFragment
         }
         final Message message;
         if (conversation.getCorrectingMessage() == null) {
-            message = new Message(conversation, body, conversation.getNextEncryption());
+            if (conversation.getReplyTo() != null) {
+                message = conversation.getReplyTo().reply();
+                message.appendBody(body);
+                message.setEncryption(conversation.getNextEncryption());
+            } else {
+                message = new Message(conversation, body, conversation.getNextEncryption());
+            }
             message.setThread(conversation.getThread());
             Message.configurePrivateMessage(message);
         } else {
@@ -910,6 +917,7 @@ public class ConversationFragment extends XmppFragment
             default:
                 sendMessage(message);
         }
+        setupReply(null);
     }
 
     private boolean trustKeysIfNeeded(final Conversation conversation, final int requestCode) {
@@ -1118,6 +1126,7 @@ public class ConversationFragment extends XmppFragment
         } else {
             activity.selectPresence(conversation, callback);
         }
+        setupReply(null);
     }
 
     private static boolean anyNeedsExternalStoragePermission(
@@ -1269,6 +1278,7 @@ public class ConversationFragment extends XmppFragment
         binding.textinput.setRichContentListener(new String[] {"image/*"}, mEditorContentListener);
 
         binding.textSendButton.setOnClickListener(this.mSendButtonListener);
+        binding.contextPreviewCancel.setOnClickListener((v) -> setupReply(null));
 
         binding.scrollToBottomButton.setOnClickListener(this.mScrollButtonListener);
         binding.messagesView.setOnScrollListener(mOnScrollListener);
@@ -1358,7 +1368,21 @@ public class ConversationFragment extends XmppFragment
     private void quoteMessage(Message message) {
         setThread(message.getThread());
         conversation.setUserSelectedThread(true);
-        quoteText(MessageUtils.prepareQuote(message));
+        if (message.getThread() == null) newThread();
+        setupReply(message);
+    }
+
+    private void setupReply(Message message) {
+        conversation.setReplyTo(message);
+        if (message == null) {
+            binding.contextPreview.setVisibility(View.GONE);
+            return;
+        }
+
+        SpannableStringBuilder body = message.getSpannableBody(null, null);
+        messageListAdapter.handleTextQuotes(body, activity.isDarkTheme());
+        binding.contextPreviewText.setText(body);
+        binding.contextPreview.setVisibility(View.VISIBLE);
     }
 
     private void setThread(Element thread) {
@@ -2721,6 +2745,7 @@ public class ConversationFragment extends XmppFragment
         }
 
         setThread(conversation.getThread());
+        setupReply(conversation.getReplyTo());
 
         stopScrolling();
         Log.d(Config.LOGTAG, "reInit(hasExtras=" + hasExtras + ")");

src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java ๐Ÿ”—

@@ -415,7 +415,7 @@ public class MessageAdapter extends ArrayAdapter<Message> {
      * Applies QuoteSpan to group of lines which starts with > or ยป characters.
      * Appends likebreaks and applies DividerSpan to them to show a padding between quote and text.
      */
-    private boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
+    public boolean handleTextQuotes(SpannableStringBuilder body, boolean darkBackground) {
         boolean startsWithQuote = false;
         int quoteDepth = 1;
         while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {

src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java ๐Ÿ”—

@@ -103,4 +103,15 @@ public class QuoteHelper {
         }
         return text;
     }
-}
+
+    public static String quote(String text) {
+        text = replaceAltQuoteCharsInText(text);
+        return text
+                // first replace all '>' at the beginning of the line with nice and tidy '>>'
+                // for nested quoting
+                .replaceAll("(^|\n)(" + QUOTE_CHAR + ")", "$1$2$2")
+                // then find all other lines and have them start with a '> '
+                .replaceAll("(^|\n)(?!" + QUOTE_CHAR + ")(.*)", "$1> $2")
+        ;
+    }
+}

src/main/java/eu/siacs/conversations/ui/widget/EditMessage.java ๐Ÿ”—

@@ -144,14 +144,7 @@ public class EditMessage extends AppCompatEditText {
     }
 
     public void insertAsQuote(String text) {
-        text = QuoteHelper.replaceAltQuoteCharsInText(text);
-        text = text
-                // first replace all '>' at the beginning of the line with nice and tidy '>>'
-                // for nested quoting
-                .replaceAll("(^|\n)(" + QuoteHelper.QUOTE_CHAR + ")", "$1$2$2")
-                // then find all other lines and have them start with a '> '
-                .replaceAll("(^|\n)(?!" + QuoteHelper.QUOTE_CHAR + ")(.*)", "$1> $2")
-        ;
+        text = QuoteHelper.quote(text);
         Editable editable = getEditableText();
         int position = getSelectionEnd();
         if (position == -1) position = editable.length();

src/main/res/layout/fragment_conversation.xml ๐Ÿ”—

@@ -48,6 +48,44 @@
                     android:transcriptMode="normal"
                     tools:listitem="@layout/message_sent"></ListView>
 
+                <LinearLayout
+                    android:id="@+id/context_preview"
+                    android:visibility="gone"
+                    android:layout_alignParentStart="true"
+                    android:layout_alignParentLeft="true"
+                    android:layout_above="@+id/textsend"
+                    android:layout_width="fill_parent"
+                    android:layout_height="wrap_content"
+                    android:paddingTop="8dp"
+                    android:paddingLeft="8dp"
+                    android:paddingRight="20dp"
+                    android:orientation="horizontal"
+                    android:background="?attr/color_background_primary">
+
+                    <ImageView
+                        android:src="?attr/icon_quote"
+                        android:layout_width="20dp"
+                        android:layout_height="20dp"
+                        android:layout_marginRight="8dp"
+                        android:contentDescription="Reply to" />
+
+                    <TextView
+                        android:id="@+id/context_preview_text"
+                        android:layout_weight="1"
+                        android:layout_width="0dp"
+                        android:layout_height="wrap_content" />
+
+                    <ImageButton
+                        android:id="@+id/context_preview_cancel"
+                        android:layout_width="20dp"
+                        android:layout_height="20dp"
+                        android:padding="0dp"
+                        android:layout_gravity="center_vertical"
+                        android:contentDescription="Cancel"
+                        android:background="?attr/color_background_primary"
+                        android:src="?attr/icon_cancel" />
+                </LinearLayout>
+
                 <RelativeLayout
                     android:id="@+id/textsend"
                     android:layout_width="fill_parent"
@@ -182,7 +220,7 @@
                     android:id="@+id/snackbar"
                     android:layout_width="fill_parent"
                     android:layout_height="wrap_content"
-                    android:layout_above="@+id/textsend"
+                    android:layout_above="@+id/context_preview"
                     android:layout_marginLeft="8dp"
                     android:layout_marginRight="8dp"
                     android:layout_marginBottom="4dp"