aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/java/de/pixart/messenger
diff options
context:
space:
mode:
authorChristian Schneppe <christian@pix-art.de>2016-12-25 21:11:20 +0100
committerChristian Schneppe <christian@pix-art.de>2016-12-25 21:11:20 +0100
commit1924cfbd53b371c6c0e5f7b5fa719ec62ed9aac6 (patch)
treeed95ad768802027c48476b141be9080bb295197f /src/main/java/de/pixart/messenger
parent37aedc49f36041abc7f35132e13241c26338ba99 (diff)
support for quoting messages
Diffstat (limited to 'src/main/java/de/pixart/messenger')
-rw-r--r--src/main/java/de/pixart/messenger/ui/ConversationFragment.java29
-rw-r--r--src/main/java/de/pixart/messenger/ui/XmppActivity.java11
-rw-r--r--src/main/java/de/pixart/messenger/ui/adapter/MessageAdapter.java170
-rw-r--r--src/main/java/de/pixart/messenger/ui/text/DividerSpan.java29
-rw-r--r--src/main/java/de/pixart/messenger/ui/text/QuoteSpan.java52
-rw-r--r--src/main/java/de/pixart/messenger/ui/widget/ListSelectionManager.java20
6 files changed, 303 insertions, 8 deletions
diff --git a/src/main/java/de/pixart/messenger/ui/ConversationFragment.java b/src/main/java/de/pixart/messenger/ui/ConversationFragment.java
index 01fc47afe..5363b3db6 100644
--- a/src/main/java/de/pixart/messenger/ui/ConversationFragment.java
+++ b/src/main/java/de/pixart/messenger/ui/ConversationFragment.java
@@ -11,6 +11,7 @@ import android.content.Intent;
import android.content.IntentSender.SendIntentException;
import android.os.Bundle;
import android.os.Handler;
+import android.text.Editable;
import android.text.InputType;
import android.util.Log;
import android.util.Pair;
@@ -519,6 +520,34 @@ public class ConversationFragment extends Fragment implements EditMessage.Keyboa
}
}
});
+ messageListAdapter.setOnQuoteListener(new MessageAdapter.OnQuoteListener() {
+
+ @Override
+ public void onQuote(String text) {
+ if (mEditMessage.isEnabled()) {
+ text = text.replaceAll("(\n *){2,}", "\n").replaceAll("(^|\n)", "$1> ").replaceAll("\n$", "");
+ Editable editable = mEditMessage.getEditableText();
+ int position = mEditMessage.getSelectionEnd();
+ if (position == -1) position = editable.length();
+ if (position > 0 && editable.charAt(position - 1) != '\n') {
+ editable.insert(position++, "\n");
+ }
+ editable.insert(position, text);
+ position += text.length();
+ editable.insert(position++, "\n");
+ if (position < editable.length() && editable.charAt(position) != '\n') {
+ editable.insert(position, "\n");
+ }
+ mEditMessage.setSelection(position);
+ mEditMessage.requestFocus();
+ InputMethodManager inputMethodManager = (InputMethodManager) getActivity()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ if (inputMethodManager != null) {
+ inputMethodManager.showSoftInput(mEditMessage, InputMethodManager.SHOW_IMPLICIT);
+ }
+ }
+ }
+ });
messagesView.setAdapter(messageListAdapter);
registerForContextMenu(messagesView);
diff --git a/src/main/java/de/pixart/messenger/ui/XmppActivity.java b/src/main/java/de/pixart/messenger/ui/XmppActivity.java
index 17d2ed3c4..9a9ad43a5 100644
--- a/src/main/java/de/pixart/messenger/ui/XmppActivity.java
+++ b/src/main/java/de/pixart/messenger/ui/XmppActivity.java
@@ -22,6 +22,7 @@ import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
+import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable;
@@ -414,6 +415,16 @@ public abstract class XmppActivity extends Activity {
}
}
+ public int getThemeResource(int r_attr_name, int r_drawable_def) {
+ int[] attrs = { r_attr_name };
+ TypedArray ta = this.getTheme().obtainStyledAttributes(attrs);
+
+ int res = ta.getResourceId(0, r_drawable_def);
+ ta.recycle();
+
+ return res;
+ }
+
protected boolean isOptimizingBattery() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
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<Message> implements CopyTextVie
private OnContactPictureLongClicked mOnContactPictureLongClickedListener;
private boolean mIndicateReceived = false;
+ private OnQuoteListener onQuoteListener;
private final ListSelectionManager listSelectionManager = new ListSelectionManager();
private HashMap<Integer, AudioWife> audioPlayer;
private boolean mUseWhiteBackground = false;
@@ -113,6 +119,10 @@ public class MessageAdapter extends ArrayAdapter<Message> 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<Message> 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<Message> 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<Message> 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<Message> 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<Message> 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<Message> 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) {
diff --git a/src/main/java/de/pixart/messenger/ui/text/DividerSpan.java b/src/main/java/de/pixart/messenger/ui/text/DividerSpan.java
new file mode 100644
index 000000000..672a267be
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/ui/text/DividerSpan.java
@@ -0,0 +1,29 @@
+package de.pixart.messenger.ui.text;
+
+import android.text.TextPaint;
+import android.text.style.MetricAffectingSpan;
+
+public class DividerSpan extends MetricAffectingSpan {
+
+ private static final float PROPORTION = 0.3f;
+
+ private final boolean large;
+
+ public DividerSpan(boolean large) {
+ this.large = large;
+ }
+
+ public boolean isLarge() {
+ return large;
+ }
+
+ @Override
+ public void updateDrawState(TextPaint tp) {
+ tp.setTextSize(tp.getTextSize() * PROPORTION);
+ }
+
+ @Override
+ public void updateMeasureState(TextPaint p) {
+ p.setTextSize(p.getTextSize() * PROPORTION);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/pixart/messenger/ui/text/QuoteSpan.java b/src/main/java/de/pixart/messenger/ui/text/QuoteSpan.java
new file mode 100644
index 000000000..a24575509
--- /dev/null
+++ b/src/main/java/de/pixart/messenger/ui/text/QuoteSpan.java
@@ -0,0 +1,52 @@
+package de.pixart.messenger.ui.text;
+
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.text.Layout;
+import android.text.TextPaint;
+import android.text.style.CharacterStyle;
+import android.text.style.LeadingMarginSpan;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+
+public class QuoteSpan extends CharacterStyle implements LeadingMarginSpan {
+
+ private final int color;
+
+ private final int width;
+ private final int paddingLeft;
+ private final int paddingRight;
+
+ private static final float WIDTH_SP = 2f;
+ private static final float PADDING_LEFT_SP = 1.5f;
+ private static final float PADDING_RIGHT_SP = 8f;
+
+ public QuoteSpan(int color, DisplayMetrics metrics) {
+ this.color = color;
+ this.width = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, WIDTH_SP, metrics);
+ this.paddingLeft = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, PADDING_LEFT_SP, metrics);
+ this.paddingRight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, PADDING_RIGHT_SP, metrics);
+ }
+
+ @Override
+ public void updateDrawState(TextPaint tp) {
+ tp.setColor(this.color);
+ }
+
+ @Override
+ public int getLeadingMargin(boolean first) {
+ return paddingLeft + width + paddingRight;
+ }
+
+ @Override
+ public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom,
+ CharSequence text, int start, int end, boolean first, Layout layout) {
+ Paint.Style style = p.getStyle();
+ int color = p.getColor();
+ p.setStyle(Paint.Style.FILL);
+ p.setColor(this.color);
+ c.drawRect(x + dir * paddingLeft, top, x + dir * (paddingLeft + width), bottom, p);
+ p.setStyle(style);
+ p.setColor(color);
+ }
+} \ No newline at end of file
diff --git a/src/main/java/de/pixart/messenger/ui/widget/ListSelectionManager.java b/src/main/java/de/pixart/messenger/ui/widget/ListSelectionManager.java
index 487cf3a0a..7ccf94dbe 100644
--- a/src/main/java/de/pixart/messenger/ui/widget/ListSelectionManager.java
+++ b/src/main/java/de/pixart/messenger/ui/widget/ListSelectionManager.java
@@ -69,8 +69,8 @@ public class ListSelectionManager {
private int futureSelectionStart;
private int futureSelectionEnd;
- public void onCreate(TextView textView) {
- final CustomCallback callback = new CustomCallback(textView);
+ public void onCreate(TextView textView, ActionMode.Callback additionalCallback) {
+ final CustomCallback callback = new CustomCallback(textView, additionalCallback);
textView.setCustomSelectionActionModeCallback(callback);
}
@@ -112,10 +112,12 @@ public class ListSelectionManager {
private class CustomCallback implements ActionMode.Callback {
private final TextView textView;
+ private final ActionMode.Callback additionalCallback;
public Object identifier;
- public CustomCallback(TextView textView) {
+ public CustomCallback(TextView textView, ActionMode.Callback additionalCallback) {
this.textView = textView;
+ this.additionalCallback = additionalCallback;
}
@Override
@@ -123,21 +125,33 @@ public class ListSelectionManager {
selectionActionMode = mode;
selectionIdentifier = identifier;
selectionTextView = textView;
+ if (additionalCallback != null) {
+ additionalCallback.onCreateActionMode(mode, menu);
+ }
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ if (additionalCallback != null) {
+ additionalCallback.onPrepareActionMode(mode, menu);
+ }
return true;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ if (additionalCallback != null && additionalCallback.onActionItemClicked(mode, item)) {
+ return true;
+ }
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
+ if (additionalCallback != null) {
+ additionalCallback.onDestroyActionMode(mode);
+ }
if (selectionActionMode == mode) {
selectionActionMode = null;
selectionIdentifier = null;