diff --git a/src/cheogram/java/com/cheogram/android/SpannedToXHTML.java b/src/cheogram/java/com/cheogram/android/SpannedToXHTML.java index 9bd67df47f88d881f3ee16ba899989d977262964..1eb13582397b167b605742c6235efe6046e22c04 100644 --- a/src/cheogram/java/com/cheogram/android/SpannedToXHTML.java +++ b/src/cheogram/java/com/cheogram/android/SpannedToXHTML.java @@ -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) { diff --git a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java index 45c0012dba51da7051c4671f9af17f54f02d4c72..5914fd9bdea8bc8ffd5f2263d525c840e2fa52c7 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/ConferenceDetailsActivity.java @@ -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(); diff --git a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java index 0d74e96fdc4ea5c62ea28467dd6d8ad80f3dab0b..bbef816e3f53e2db89f7c8901468cfd1e5da27f9 100644 --- a/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java +++ b/src/main/java/eu/siacs/conversations/ui/ConversationFragment.java @@ -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); diff --git a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java index 7b7e243ef43e2e26dfe8b0a021674a7aee29e529..1691e75dd9406e0ff7a1bf62402939e2524f051a 100644 --- a/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java +++ b/src/main/java/eu/siacs/conversations/ui/adapter/MessageAdapter.java @@ -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 { 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 { 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 { */ 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 { 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 { 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 { 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( diff --git a/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java b/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java index 883c5507a9dae04016c4dd71a4822cffbaa812f1..d835f862a1804f796638c5e64d443b70fbc7c0e3 100644 --- a/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java +++ b/src/main/java/eu/siacs/conversations/ui/util/QuoteHelper.java @@ -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; } diff --git a/src/main/java/eu/siacs/conversations/utils/StylingHelper.java b/src/main/java/eu/siacs/conversations/utils/StylingHelper.java index c5f1693888c90e4c318ca47c8faaf081c337fa4f..95e51eebe427d1b4adce360f8a1ae354740e4079 100644 --- a/src/main/java/eu/siacs/conversations/utils/StylingHelper.java +++ b/src/main/java/eu/siacs/conversations/utils/StylingHelper.java @@ -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> 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); } } }