Detailed changes
@@ -39,7 +39,7 @@ public class SpannedToXHTML {
ParcelableSpan[] spans = newText.getSpans(0, newText.length(), ParcelableSpan.class);
for (final var span : spans) {
final var userFlags = (text.getSpanFlags(span) & Spanned.SPAN_USER) >> Spanned.SPAN_USER_SHIFT;
- if (span instanceof SuggestionSpan || userFlags == 1) newText.removeSpan(span);
+ if (span instanceof SuggestionSpan || userFlags == StylingHelper.XHTML_IGNORE) newText.removeSpan(span);
}
BaseInputConnection.removeComposingSpans(newText);
return newText;
@@ -85,13 +85,26 @@ public class SpannedToXHTML {
next = text.nextSpanTransition(i, end, CharacterStyle.class);
CharacterStyle[] style = text.getSpans(i, next, CharacterStyle.class);
for (int j = 0; j < style.length; j++) {
+ final var userFlags = (text.getSpanFlags(style[j]) & Spanned.SPAN_USER) >> Spanned.SPAN_USER_SHIFT;
+ if (userFlags == StylingHelper.XHTML_REMOVE) {
+ continue outer;
+ }
+
if (style[j] instanceof StyleSpan) {
int s = ((StyleSpan) style[j]).getStyle();
if ((s & Typeface.BOLD) != 0) {
- out = out.addChild("b");
+ if (userFlags == StylingHelper.XHTML_EMPHASIS) {
+ out = out.addChild("strong");
+ } else {
+ out = out.addChild("b");
+ }
}
if ((s & Typeface.ITALIC) != 0) {
- out = out.addChild("i");
+ if (userFlags == StylingHelper.XHTML_EMPHASIS) {
+ out = out.addChild("em");
+ } else {
+ out = out.addChild("i");
+ }
}
}
if (style[j] instanceof TypefaceSpan) {
@@ -259,7 +259,7 @@ public class ConferenceDetailsActivity extends XmppActivity implements OnConvers
this.binding.editMucNameButton.setOnClickListener(this::onMucEditButtonClicked);
this.binding.mucEditTitle.addTextChangedListener(this);
this.binding.mucEditSubject.addTextChangedListener(this);
- this.binding.mucEditSubject.addTextChangedListener(new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject));
+ //this.binding.mucEditSubject.addTextChangedListener(new StylingHelper.MessageEditorStyler(this.binding.mucEditSubject));
this.binding.editTags.addTextChangedListener(this);
this.mMediaAdapter = new MediaAdapter(this, R.dimen.media_size);
this.mUserPreviewAdapter = new UserPreviewAdapter();
@@ -1383,9 +1383,6 @@ public class ConversationFragment extends XmppFragment
DataBindingUtil.inflate(inflater, R.layout.fragment_conversation, container, false);
binding.getRoot().setOnClickListener(null); // TODO why the fuck did we do this?
- binding.textinput.addTextChangedListener(
- new StylingHelper.MessageEditorStyler(binding.textinput));
-
binding.textinput.setOnEditorActionListener(mEditorActionListener);
binding.textinput.setRichContentListener(new String[] {"image/*"}, mEditorContentListener);
DisplayMetrics displayMetrics = new DisplayMetrics();
@@ -1416,6 +1413,9 @@ public class ConversationFragment extends XmppFragment
messageListAdapter.setConversationFragment(this);
binding.messagesView.setAdapter(messageListAdapter);
+ binding.textinput.addTextChangedListener(
+ new StylingHelper.MessageEditorStyler(binding.textinput, messageListAdapter));
+
registerForContextMenu(binding.messagesView);
registerForContextMenu(binding.textSendButton);
@@ -12,6 +12,8 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.preference.PreferenceManager;
+import android.text.Editable;
+import android.text.Spanned;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
@@ -458,17 +460,18 @@ public class MessageAdapter extends ArrayAdapter<Message> {
private void applyQuoteSpan(
final TextView textView,
- SpannableStringBuilder body,
+ Editable body,
int start,
int end,
- final BubbleColor bubbleColor) {
- if (start > 1 && !"\n\n".equals(body.subSequence(start - 2, start).toString())) {
+ final BubbleColor bubbleColor,
+ final boolean makeEdits) {
+ if (makeEdits && 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())) {
+ if (makeEdits && end < body.length() - 1 && !"\n\n".equals(body.subSequence(end, end + 2).toString())) {
body.insert(end, "\n");
body.setSpan(
new DividerSpan(false),
@@ -485,10 +488,14 @@ public class MessageAdapter extends ArrayAdapter<Message> {
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
- public boolean handleTextQuotes(final TextView textView, final SpannableStringBuilder body) {
+ public boolean handleTextQuotes(final TextView textView, final Editable body) {
+ return handleTextQuotes(textView, body, true);
+ }
+
+ public boolean handleTextQuotes(final TextView textView, final Editable body, final boolean deleteMarkers) {
final boolean colorfulBackground = this.bubbleDesign.colorfulChatBubbles;
final BubbleColor bubbleColor = colorfulBackground ? BubbleColor.SECONDARY : BubbleColor.SURFACE;
- return handleTextQuotes(textView, body, bubbleColor);
+ return handleTextQuotes(textView, body, bubbleColor, deleteMarkers);
}
/**
@@ -497,8 +504,9 @@ public class MessageAdapter extends ArrayAdapter<Message> {
*/
public boolean handleTextQuotes(
final TextView textView,
- final SpannableStringBuilder body,
- final BubbleColor bubbleColor) {
+ final Editable body,
+ final BubbleColor bubbleColor,
+ final boolean deleteMarkers) {
boolean startsWithQuote = false;
int quoteDepth = 1;
while (QuoteHelper.bodyContainsQuoteStart(body) && quoteDepth <= Config.QUOTE_MAX_DEPTH) {
@@ -506,18 +514,23 @@ public class MessageAdapter extends ArrayAdapter<Message> {
int lineStart = -1;
int lineTextStart = -1;
int quoteStart = -1;
+ int skipped = 0;
for (int i = 0; i <= body.length(); i++) {
+ if (!deleteMarkers && QuoteHelper.isRelativeSizeSpanned(body, i)) {
+ skipped++;
+ continue;
+ }
char current = body.length() > i ? body.charAt(i) : '\n';
if (lineStart == -1) {
if (previous == '\n') {
if (i < body.length() && QuoteHelper.isPositionQuoteStart(body, i)) {
// Line start with quote
lineStart = i;
- if (quoteStart == -1) quoteStart = i;
+ if (quoteStart == -1) quoteStart = i - skipped;
if (i == 0) startsWithQuote = true;
} else if (quoteStart >= 0) {
// Line start without quote, apply spans there
- applyQuoteSpan(textView, body, quoteStart, i - 1, bubbleColor);
+ applyQuoteSpan(textView, body, quoteStart, i - 1, bubbleColor, deleteMarkers);
quoteStart = -1;
}
}
@@ -528,21 +541,26 @@ public class MessageAdapter extends ArrayAdapter<Message> {
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++, " ");
+ if (deleteMarkers) {
+ i -= lineTextStart - lineStart;
+ body.delete(lineStart, lineTextStart);
+ if (i == lineStart) {
+ // Avoid empty lines because span over empty line can be hidden
+ body.insert(i++, " ");
+ }
+ } else {
+ body.setSpan(new RelativeSizeSpan(i - (lineTextStart - lineStart) == lineStart ? 1 : 0), lineStart, lineTextStart, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE | StylingHelper.XHTML_REMOVE << Spanned.SPAN_USER_SHIFT);
}
lineStart = -1;
lineTextStart = -1;
}
}
previous = current;
+ skipped = 0;
}
if (quoteStart >= 0) {
// Apply spans to finishing open quote
- applyQuoteSpan(textView, body, quoteStart, body.length(), bubbleColor);
+ applyQuoteSpan(textView, body, quoteStart, body.length(), bubbleColor, deleteMarkers);
}
quoteDepth++;
}
@@ -613,9 +631,9 @@ public class MessageAdapter extends ArrayAdapter<Message> {
int start = body.getSpanStart(quote);
int end = body.getSpanEnd(quote);
body.removeSpan(quote);
- applyQuoteSpan(viewHolder.messageBody, body, start, end, bubbleColor);
+ applyQuoteSpan(viewHolder.messageBody, body, start, end, bubbleColor, true);
}
- boolean startsWithQuote = handleTextQuotes(viewHolder.messageBody, body, bubbleColor);
+ boolean startsWithQuote = handleTextQuotes(viewHolder.messageBody, body, bubbleColor, true);
if (!message.isPrivateMessage()) {
if (hasMeCommand) {
body.setSpan(
@@ -1,5 +1,8 @@
package eu.siacs.conversations.ui.util;
+import android.text.Spanned;
+import android.text.style.RelativeSizeSpan;
+
import eu.siacs.conversations.Config;
import eu.siacs.conversations.utils.UIHelper;
@@ -11,6 +14,14 @@ public class QuoteHelper {
public static final char QUOTE_ALT_CHAR = 'ยป';
public static final char QUOTE_ALT_END_CHAR = 'ยซ';
+ public static boolean isRelativeSizeSpanned(Spanned body, int pos) {
+ for (final var span : body.getSpans(pos, pos, RelativeSizeSpan.class)) {
+ if (body.getSpanEnd(span) != pos) return true;
+ }
+
+ return false;
+ }
+
public static boolean isPositionQuoteCharacter(CharSequence body, int pos) {
// second part of logical check actually goes against the logic indicated in the method name, since it also checks for context
// but it's very useful
@@ -43,6 +54,9 @@ public class QuoteHelper {
* 'Prequote' means anything we require or can accept in front of a QuoteChar.
*/
public static boolean isPositionPrecededByPreQuote(CharSequence body, int pos) {
+ if (body instanceof Spanned) {
+ if (isRelativeSizeSpanned((Spanned) body, pos - 1)) return true;
+ }
return UIHelper.isPositionPrecededByLineStart(body, pos);
}
@@ -55,6 +69,9 @@ public class QuoteHelper {
public static boolean bodyContainsQuoteStart(CharSequence body) {
for (int i = 0; i < body.length(); i++) {
+ if (body instanceof Spanned) {
+ if (isRelativeSizeSpanned((Spanned) body, i)) continue;
+ }
if (isPositionQuoteStart(body, i)) {
return true;
}
@@ -40,6 +40,7 @@ import android.text.Spanned;
import android.text.TextWatcher;
import android.text.style.BackgroundColorSpan;
import android.text.style.ForegroundColorSpan;
+import android.text.style.RelativeSizeSpan;
import android.text.style.StrikethroughSpan;
import android.text.style.StyleSpan;
import android.text.style.TypefaceSpan;
@@ -57,10 +58,15 @@ import java.util.List;
import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Message;
+import eu.siacs.conversations.ui.adapter.MessageAdapter;
import eu.siacs.conversations.ui.text.QuoteSpan;
public class StylingHelper {
+ public static final int XHTML_IGNORE = 1;
+ public static final int XHTML_REMOVE = 2;
+ public static final int XHTML_EMPHASIS = 3;
+
private static final List<? extends Class<? extends ParcelableSpan>> SPAN_CLASSES = Arrays.asList(
StyleSpan.class,
StrikethroughSpan.class,
@@ -80,8 +86,8 @@ public class StylingHelper {
public static void format(final Editable editable, int start, int end, @ColorInt int textColor, final boolean composing) {
for (ImStyleParser.Style style : ImStyleParser.parse(editable, start, end)) {
final int keywordLength = style.getKeyword().length();
- editable.setSpan(createSpanForStyle(style), style.getStart() + keywordLength, style.getEnd() - keywordLength + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | (composing ? 1 << Spanned.SPAN_USER_SHIFT : 0));
- makeKeywordOpaque(editable, style.getStart(), style.getStart() + keywordLength, textColor, composing);
+ editable.setSpan(createSpanForStyle(style), style.getStart() + keywordLength, style.getEnd() - keywordLength + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | ("*".equals(style.getKeyword()) || "_".equals(style.getKeyword()) ? XHTML_EMPHASIS << Spanned.SPAN_USER_SHIFT : 0));
+ makeKeywordOpaque(editable, style.getStart(), style.getStart() + keywordLength + ("```".equals(style.getKeyword()) ? 1 : 0), textColor, composing);
makeKeywordOpaque(editable, style.getEnd() - keywordLength + 1, style.getEnd() + 1, textColor, composing);
}
}
@@ -194,7 +200,7 @@ public class StylingHelper {
QuoteSpan[] quoteSpans = editable.getSpans(start, end, QuoteSpan.class);
@ColorInt int textColor = quoteSpans.length > 0 ? quoteSpans[0].getColor() : fallbackTextColor;
@ColorInt int keywordColor = transformColor(textColor);
- editable.setSpan(new ForegroundColorSpan(keywordColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | (composing ? 1 << Spanned.SPAN_USER_SHIFT : 0));
+ editable.setSpan(new ForegroundColorSpan(keywordColor), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | (composing ? XHTML_REMOVE << Spanned.SPAN_USER_SHIFT : 0));
}
private static
@@ -225,9 +231,11 @@ public class StylingHelper {
public static class MessageEditorStyler implements TextWatcher {
private final EditText mEditText;
+ private final MessageAdapter mAdapter;
- public MessageEditorStyler(EditText editText) {
+ public MessageEditorStyler(EditText editText, MessageAdapter adapter) {
this.mEditText = editText;
+ this.mAdapter = adapter;
}
@Override
@@ -243,7 +251,14 @@ public class StylingHelper {
@Override
public void afterTextChanged(Editable editable) {
clear(editable);
+ for (final var span : editable.getSpans(0, editable.length() - 1, QuoteSpan.class)) {
+ editable.removeSpan(span);
+ }
+ for (final var span : editable.getSpans(0, editable.length() - 1, RelativeSizeSpan.class)) {
+ editable.removeSpan(span);
+ }
format(editable, mEditText.getCurrentTextColor(), true);
+ mAdapter.handleTextQuotes(mEditText, editable, false);
}
}
}