Detailed changes
@@ -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>
@@ -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>
@@ -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)) {
@@ -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;
}
@@ -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 + ")");
@@ -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) {
@@ -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")
+ ;
+ }
+}
@@ -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();
@@ -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"