From 1924cfbd53b371c6c0e5f7b5fa719ec62ed9aac6 Mon Sep 17 00:00:00 2001 From: Christian Schneppe Date: Sun, 25 Dec 2016 21:11:20 +0100 Subject: support for quoting messages --- .../messenger/ui/adapter/MessageAdapter.java | 170 ++++++++++++++++++++- 1 file changed, 165 insertions(+), 5 deletions(-) (limited to 'src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java') diff --git a/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java b/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java index 71742281d..0b98614b7 100644 --- a/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java +++ b/src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java @@ -25,7 +25,10 @@ import android.text.util.Linkify; import android.util.DisplayMetrics; import android.util.Log; import android.util.Patterns; +import android.view.ActionMode; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnLongClickListener; @@ -62,6 +65,8 @@ import de.pixart.messenger.entities.Transferable; import de.pixart.messenger.persistance.FileBackend; import de.pixart.messenger.ui.ConversationActivity; import de.pixart.messenger.ui.ShowFullscreenMessageActivity; +import de.pixart.messenger.ui.text.DividerSpan; +import de.pixart.messenger.ui.text.QuoteSpan; import de.pixart.messenger.ui.widget.ClickableMovementMethod; import de.pixart.messenger.ui.widget.CopyTextView; import de.pixart.messenger.ui.widget.ListSelectionManager; @@ -93,6 +98,7 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie private OnContactPictureLongClicked mOnContactPictureLongClickedListener; private boolean mIndicateReceived = false; + private OnQuoteListener onQuoteListener; private final ListSelectionManager listSelectionManager = new ListSelectionManager(); private HashMap audioPlayer; private boolean mUseWhiteBackground = false; @@ -113,6 +119,10 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie this.mOnContactPictureLongClickedListener = listener; } + public void setOnQuoteListener(OnQuoteListener listener) { + this.onQuoteListener = listener; + } + @Override public int getViewTypeCount() { return 3; @@ -302,7 +312,7 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie viewHolder.messageBody.setIncludeFontPadding(false); Spannable span = new SpannableString(body); span.setSpan(new RelativeSizeSpan(4.0f), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - span.setSpan(new ForegroundColorSpan(activity.getWarningTextColor()), 0, body.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + span.setSpan(new ForegroundColorSpan(activity.getWarningTextColor()), 0, body.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); viewHolder.messageBody.setText(span); } @@ -328,6 +338,74 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie } + private int applyQuoteSpan(SpannableStringBuilder body, int start, int end, boolean darkBackground) { + if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) { + body.insert(start++, "\n"); + body.setSpan(new DividerSpan(false), start - 2, start, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + end++; + } + if (end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) { + body.insert(end, "\n"); + body.setSpan(new DividerSpan(false), end, end + 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + int color = darkBackground ? this.getMessageTextColor(darkBackground, false) + : getContext().getResources().getColor(R.color.bubble); + DisplayMetrics metrics = getContext().getResources().getDisplayMetrics(); + body.setSpan(new QuoteSpan(color, metrics), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + return 0; + } + + /** + * 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) { + boolean startsWithQuote = false; + char previous = '\n'; + int lineStart = -1; + int lineTextStart = -1; + int quoteStart = -1; + for (int i = 0; i <= body.length(); i++) { + char current = body.length() > i ? body.charAt(i) : '\n'; + if (lineStart == -1) { + if (previous == '\n') { + if (current == '>' || current == '\u00bb') { + // Line start with quote + lineStart = i; + if (quoteStart == -1) quoteStart = i; + if (i == 0) startsWithQuote = true; + } else if (quoteStart >= 0) { + // Line start without quote, apply spans there + applyQuoteSpan(body, quoteStart, i - 1, darkBackground); + quoteStart = -1; + } + } + } else { + // Remove extra spaces between > and first character in the line + // > character will be removed too + if (current != ' ' && lineTextStart == -1) { + lineTextStart = i; + } + if (current == '\n') { + body.delete(lineStart, lineTextStart); + i -= lineTextStart - lineStart; + if (i == lineStart) { + // Avoid empty lines because span over empty line can be hidden + body.insert(i++, " "); + } + lineStart = -1; + lineTextStart = -1; + } + } + previous = current; + } + if (quoteStart >= 0) { + // Apply spans to finishing open quote + applyQuoteSpan(body, quoteStart, body.length(), darkBackground); + } + return startsWithQuote; + } + private void displayTextMessage(final ViewHolder viewHolder, final Message message, boolean darkBackground) { if (viewHolder.download_button != null) { viewHolder.download_button.setVisibility(View.GONE); @@ -350,8 +428,9 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie for (Message.MergeSeparator mergeSeparator : mergeSeparators) { int start = body.getSpanStart(mergeSeparator); int end = body.getSpanEnd(mergeSeparator); - body.setSpan(new RelativeSizeSpan(0.3f), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + body.setSpan(new DividerSpan(true), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } + boolean startsWithQuote = handleTextQuotes(body, darkBackground); if (message.getType() != Message.TYPE_PRIVATE) { if (hasMeCommand) { body.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), 0, nick.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); @@ -372,7 +451,13 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie } body.insert(0, privateMarker); int privateMarkerIndex = privateMarker.length(); - body.insert(privateMarkerIndex, " "); + if (startsWithQuote) { + body.insert(privateMarkerIndex, "\n\n"); + body.setSpan(new DividerSpan(false), privateMarkerIndex, privateMarkerIndex + 2, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + body.insert(privateMarkerIndex, " "); + } body.setSpan(new ForegroundColorSpan(getMessageTextColor(darkBackground, false)), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); body.setSpan(new StyleSpan(Typeface.BOLD), 0, privateMarkerIndex, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); if (hasMeCommand) { @@ -641,7 +726,7 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie break; } if (viewHolder.messageBody != null) { - listSelectionManager.onCreate(viewHolder.messageBody); + listSelectionManager.onCreate(viewHolder.messageBody, new MessageBodyActionModeCallback(viewHolder.messageBody)); viewHolder.messageBody.setCopyHandler(this); } view.setTag(viewHolder); @@ -813,9 +898,84 @@ public class MessageAdapter extends ArrayAdapter implements CopyTextVie listSelectionManager.onAfterNotifyDataSetChanged(); } + private String transformText(CharSequence text, int start, int end, boolean forCopy) { + SpannableStringBuilder builder = new SpannableStringBuilder(text); + Object copySpan = new Object(); + builder.setSpan(copySpan, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + DividerSpan[] dividerSpans = builder.getSpans(0, builder.length(), DividerSpan.class); + for (DividerSpan dividerSpan : dividerSpans) { + builder.replace(builder.getSpanStart(dividerSpan), builder.getSpanEnd(dividerSpan), + dividerSpan.isLarge() ? "\n\n" : "\n"); + } + start = builder.getSpanStart(copySpan); + end = builder.getSpanEnd(copySpan); + if (start == -1 || end == -1) return ""; + builder = new SpannableStringBuilder(builder, start, end); + if (forCopy) { + QuoteSpan[] quoteSpans = builder.getSpans(0, builder.length(), QuoteSpan.class); + for (QuoteSpan quoteSpan : quoteSpans) { + builder.insert(builder.getSpanStart(quoteSpan), "> "); + } + } + return builder.toString(); + } + @Override public String transformTextForCopy(CharSequence text, int start, int end) { - return text.toString().substring(start, end); + if (text instanceof Spanned) { + return transformText(text, start, end, true); + } else { + return text.toString().substring(start, end); + } + } + + public interface OnQuoteListener { + public void onQuote(String text); + } + + private class MessageBodyActionModeCallback implements ActionMode.Callback { + + private final TextView textView; + + public MessageBodyActionModeCallback(TextView textView) { + this.textView = textView; + } + + @Override + public boolean onCreateActionMode(ActionMode mode, Menu menu) { + if (onQuoteListener != null) { + int quoteResId = activity.getThemeResource(R.attr.icon_quote, R.drawable.ic_action_reply); + // 3rd item is placed after "copy" item + menu.add(0, android.R.id.button1, 3, R.string.quote).setIcon(quoteResId) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT); + } + return false; + } + + @Override + public boolean onPrepareActionMode(ActionMode mode, Menu menu) { + return false; + } + + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + if (item.getItemId() == android.R.id.button1) { + int start = textView.getSelectionStart(); + int end = textView.getSelectionEnd(); + if (end > start) { + String text = transformText(textView.getText(), start, end, false); + if (onQuoteListener != null) { + onQuoteListener.onQuote(text); + } + mode.finish(); + } + return true; + } + return false; + } + + @Override + public void onDestroyActionMode(ActionMode mode) {} } public void openDownloadable(Message message) { -- cgit v1.2.3