Add mentions for v2 group chats.

This commit is contained in:
Cody Henthorne
2020-08-05 16:45:52 -04:00
committed by Greyson Parrelli
parent 0bb9c1d650
commit b2d4c5d14b
90 changed files with 2279 additions and 372 deletions

View File

@@ -5,6 +5,7 @@ import android.content.res.Configuration;
import android.graphics.Canvas;
import android.os.Build;
import android.os.Bundle;
import android.text.Annotation;
import android.text.Editable;
import android.text.InputType;
import android.text.Spannable;
@@ -13,7 +14,6 @@ import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.text.method.QwertyKeyListener;
import android.text.style.RelativeSizeSpan;
import android.util.AttributeSet;
import android.view.inputmethod.EditorInfo;
@@ -21,7 +21,6 @@ import android.view.inputmethod.InputConnection;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.os.BuildCompat;
import androidx.core.view.inputmethod.EditorInfoCompat;
import androidx.core.view.inputmethod.InputConnectionCompat;
import androidx.core.view.inputmethod.InputContentInfoCompat;
@@ -30,18 +29,26 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.components.mention.MentionDeleter;
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
import org.thoughtcrime.securesms.components.mention.MentionValidatorWatcher;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.StringUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.ThemeUtil;
import java.util.UUID;
import java.util.List;
import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER;
public class ComposeText extends EmojiEditText {
private CharSequence hint;
private SpannableString subHint;
private CharSequence combinedHint;
private MentionRendererDelegate mentionRendererDelegate;
private MentionValidatorWatcher mentionValidatorWatcher;
@Nullable private InputPanel.MediaListener mediaListener;
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
@@ -62,47 +69,63 @@ public class ComposeText extends EmojiEditText {
initialize();
}
public String getTextTrimmed(){
return getText().toString().trim();
/**
* Trims and returns text while preserving potential spans like {@link MentionAnnotation}.
*/
public @NonNull CharSequence getTextTrimmed() {
Editable text = getText();
if (text == null) {
return "";
}
return StringUtil.trimSequence(text);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (!TextUtils.isEmpty(hint)) {
if (!TextUtils.isEmpty(subHint)) {
setHint(new SpannableStringBuilder().append(ellipsizeToWidth(hint))
.append("\n")
.append(ellipsizeToWidth(subHint)));
} else {
setHint(ellipsizeToWidth(hint));
}
if (!TextUtils.isEmpty(combinedHint)) {
setHint(combinedHint);
}
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
super.onSelectionChanged(selStart, selEnd);
protected void onSelectionChanged(int selectionStart, int selectionEnd) {
super.onSelectionChanged(selectionStart, selectionEnd);
if (FeatureFlags.mentions()) {
if (selStart == selEnd) {
doAfterCursorChange();
if (FeatureFlags.mentions() && getText() != null) {
boolean selectionChanged = changeSelectionForPartialMentions(getText(), selectionStart, selectionEnd);
if (selectionChanged) {
return;
}
if (selectionStart == selectionEnd) {
doAfterCursorChange(getText());
} else {
updateQuery("");
}
}
if (cursorPositionChangedListener != null) {
cursorPositionChangedListener.onCursorPositionChanged(selStart, selEnd);
cursorPositionChangedListener.onCursorPositionChanged(selectionStart, selectionEnd);
}
}
@Override
protected void onDraw(Canvas canvas) {
if (FeatureFlags.mentions() && getText() != null && getLayout() != null) {
if (getText() != null && getLayout() != null) {
int checkpoint = canvas.save();
// Clip using same logic as TextView drawing
int maxScrollY = getLayout().getHeight() - getBottom() - getTop() - getCompoundPaddingBottom() - getCompoundPaddingTop();
float clipLeft = getCompoundPaddingLeft() + getScrollX();
float clipTop = (getScrollY() == 0) ? 0 : getExtendedPaddingTop() + getScrollY();
float clipRight = getRight() - getLeft() - getCompoundPaddingRight() + getScrollX();
float clipBottom = getBottom() - getTop() + getScrollY() - ((getScrollY() == maxScrollY) ? 0 : getExtendedPaddingBottom());
canvas.clipRect(clipLeft - 10, clipTop, clipRight + 10, clipBottom);
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
try {
mentionRendererDelegate.draw(canvas, getText(), getLayout());
} finally {
@@ -120,25 +143,25 @@ public class ComposeText extends EmojiEditText {
}
public void setHint(@NonNull String hint, @Nullable CharSequence subHint) {
this.hint = hint;
if (subHint != null) {
this.subHint = new SpannableString(subHint);
this.subHint.setSpan(new RelativeSizeSpan(0.5f), 0, subHint.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
Spannable subHintSpannable = new SpannableString(subHint);
subHintSpannable.setSpan(new RelativeSizeSpan(0.5f), 0, subHintSpannable.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
combinedHint = new SpannableStringBuilder().append(ellipsizeToWidth(hint))
.append("\n")
.append(ellipsizeToWidth(subHintSpannable));
} else {
this.subHint = null;
combinedHint = ellipsizeToWidth(hint);
}
if (this.subHint != null) {
super.setHint(new SpannableStringBuilder().append(ellipsizeToWidth(this.hint))
.append("\n")
.append(ellipsizeToWidth(this.subHint)));
} else {
super.setHint(ellipsizeToWidth(this.hint));
}
super.setHint(combinedHint);
}
public void appendInvite(String invite) {
if (getText() == null) {
return;
}
if (!TextUtils.isEmpty(getText()) && !getText().toString().equals(" ")) {
append(" ");
}
@@ -155,13 +178,18 @@ public class ComposeText extends EmojiEditText {
this.mentionQueryChangedListener = listener;
}
public void setMentionValidator(@Nullable MentionValidatorWatcher.MentionValidator mentionValidator) {
if (FeatureFlags.mentions()) {
mentionValidatorWatcher.setMentionValidator(mentionValidator);
}
}
private boolean isLandscape() {
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
}
public void setTransport(TransportOption transport) {
final boolean useSystemEmoji = TextSecurePreferences.isSystemEmojiPreferred(getContext());
final boolean isIncognito = TextSecurePreferences.isIncognitoKeyboardEnabled(getContext());
int imeOptions = (getImeOptions() & ~EditorInfo.IME_MASK_ACTION) | EditorInfo.IME_ACTION_SEND;
int inputType = getInputType();
@@ -201,19 +229,59 @@ public class ComposeText extends EmojiEditText {
this.mediaListener = mediaListener;
}
public boolean hasMentions() {
Editable text = getText();
if (text != null) {
return !MentionAnnotation.getMentionAnnotations(text).isEmpty();
}
return false;
}
public @NonNull List<Mention> getMentions() {
return MentionAnnotation.getMentionsFromAnnotations(getText());
}
private void initialize() {
if (TextSecurePreferences.isIncognitoKeyboardEnabled(getContext())) {
setImeOptions(getImeOptions() | 16777216);
}
mentionRendererDelegate = new MentionRendererDelegate(getContext(), ThemeUtil.getThemedColor(getContext(), R.attr.conversation_mention_background_color));
if (FeatureFlags.mentions()) {
mentionRendererDelegate = new MentionRendererDelegate(getContext());
addTextChangedListener(new MentionDeleter());
mentionValidatorWatcher = new MentionValidatorWatcher();
addTextChangedListener(mentionValidatorWatcher);
}
}
private void doAfterCursorChange() {
Editable text = getText();
if (text != null && enoughToFilter(text)) {
private boolean changeSelectionForPartialMentions(@NonNull Spanned spanned, int selectionStart, int selectionEnd) {
Annotation[] annotations = spanned.getSpans(0, spanned.length(), Annotation.class);
for (Annotation annotation : annotations) {
if (MentionAnnotation.isMentionAnnotation(annotation)) {
int spanStart = spanned.getSpanStart(annotation);
int spanEnd = spanned.getSpanEnd(annotation);
boolean startInMention = selectionStart > spanStart && selectionStart < spanEnd;
boolean endInMention = selectionEnd > spanStart && selectionEnd < spanEnd;
if (startInMention || endInMention) {
if (selectionStart == selectionEnd) {
setSelection(spanEnd, spanEnd);
} else {
int newStart = startInMention ? spanStart : selectionStart;
int newEnd = endInMention ? spanEnd : selectionEnd;
setSelection(newStart, newEnd);
}
return true;
}
}
}
return false;
}
private void doAfterCursorChange(@NonNull Editable text) {
if (enoughToFilter(text)) {
performFiltering(text);
} else {
updateQuery("");
@@ -241,7 +309,7 @@ public class ComposeText extends EmojiEditText {
return end - findQueryStart(text, end) >= 1;
}
public void replaceTextWithMention(@NonNull String displayName, @NonNull UUID uuid) {
public void replaceTextWithMention(@NonNull String displayName, @NonNull RecipientId recipientId) {
Editable text = getText();
if (text == null) {
return;
@@ -251,14 +319,12 @@ public class ComposeText extends EmojiEditText {
int end = getSelectionEnd();
int start = findQueryStart(text, end) - 1;
String original = TextUtils.substring(text, start, end);
QwertyKeyListener.markAsReplaced(text, start, end, original);
text.replace(start, end, createReplacementToken(displayName, uuid));
text.replace(start, end, createReplacementToken(displayName, recipientId));
}
private @NonNull CharSequence createReplacementToken(@NonNull CharSequence text, @NonNull UUID uuid) {
SpannableStringBuilder builder = new SpannableStringBuilder("@");
private @NonNull CharSequence createReplacementToken(@NonNull CharSequence text, @NonNull RecipientId recipientId) {
SpannableStringBuilder builder = new SpannableStringBuilder().append(MENTION_STARTER);
if (text instanceof Spanned) {
SpannableString spannableString = new SpannableString(text + " ");
TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, spannableString, 0);
@@ -267,7 +333,7 @@ public class ComposeText extends EmojiEditText {
builder.append(text).append(" ");
}
builder.setSpan(MentionAnnotation.mentionAnnotationForUuid(uuid), 0, builder.length() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
builder.setSpan(MentionAnnotation.mentionAnnotationForRecipientId(recipientId), 0, builder.length() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return builder;
}
@@ -278,11 +344,11 @@ public class ComposeText extends EmojiEditText {
}
int delimiterSearchIndex = inputCursorPosition - 1;
while (delimiterSearchIndex >= 0 && (text.charAt(delimiterSearchIndex) != '@' && text.charAt(delimiterSearchIndex) != ' ')) {
while (delimiterSearchIndex >= 0 && (text.charAt(delimiterSearchIndex) != MENTION_STARTER && text.charAt(delimiterSearchIndex) != ' ')) {
delimiterSearchIndex--;
}
if (delimiterSearchIndex >= 0 && text.charAt(delimiterSearchIndex) == '@') {
if (delimiterSearchIndex >= 0 && text.charAt(delimiterSearchIndex) == MENTION_STARTER) {
return delimiterSearchIndex + 1;
}
return inputCursorPosition;
@@ -300,7 +366,7 @@ public class ComposeText extends EmojiEditText {
@Override
public boolean onCommitContent(InputContentInfoCompat inputContentInfo, int flags, Bundle opts) {
if (BuildCompat.isAtLeastNMR1() && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
if (Build.VERSION.SDK_INT >= 25 && (flags & InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION) != 0) {
try {
inputContentInfo.requestPermission();
} catch (Exception e) {

View File

@@ -2,10 +2,8 @@ package org.thoughtcrime.securesms.components;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.KeyEvent;
@@ -94,7 +92,6 @@ public class InputPanel extends LinearLayout
super(context, attrs);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public InputPanel(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@@ -160,7 +157,7 @@ public class InputPanel extends LinearLayout
public void setQuote(@NonNull GlideRequests glideRequests,
long id,
@NonNull Recipient author,
@NonNull String body,
@NonNull CharSequence body,
@NonNull SlideDeck attachments)
{
this.quoteView.setQuote(glideRequests, id, author, body, false, attachments);
@@ -228,7 +225,7 @@ public class InputPanel extends LinearLayout
public Optional<QuoteModel> getQuote() {
if (quoteView.getQuoteId() > 0 && quoteView.getVisibility() == View.VISIBLE) {
return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getId(), quoteView.getBody(), false, quoteView.getAttachments()));
return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getId(), quoteView.getBody().toString(), false, quoteView.getAttachments(), quoteView.getMentions()));
} else {
return Optional.absent();
}

View File

@@ -23,6 +23,8 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
@@ -55,7 +57,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
private long id;
private LiveRecipient author;
private String body;
private CharSequence body;
private TextView mediaDescriptionText;
private TextView missingLinkText;
private SlideDeck attachments;
@@ -147,7 +149,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
public void setQuote(GlideRequests glideRequests,
long id,
@NonNull Recipient author,
@Nullable String body,
@Nullable CharSequence body,
boolean originalMissing,
@NonNull SlideDeck attachments)
{
@@ -196,7 +198,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
mainView.setBackgroundColor(author.getColor().toQuoteBackgroundColor(getContext(), outgoing));
}
private void setQuoteText(@Nullable String body, @NonNull SlideDeck attachments) {
private void setQuoteText(@Nullable CharSequence body, @NonNull SlideDeck attachments) {
if (!TextUtils.isEmpty(body) || !attachments.containsMediaSlide()) {
bodyView.setVisibility(VISIBLE);
bodyView.setText(body == null ? "" : body);
@@ -280,11 +282,15 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
return author.get();
}
public String getBody() {
public CharSequence getBody() {
return body;
}
public List<Attachment> getAttachments() {
return attachments.asAttachments();
}
public @NonNull List<Mention> getMentions() {
return MentionAnnotation.getMentionsFromAnnotations(body);
}
}

View File

@@ -2,12 +2,17 @@ package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.core.widget.TextViewCompat;
import androidx.appcompat.widget.AppCompatTextView;
import android.text.Annotation;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
@@ -15,10 +20,15 @@ import android.util.TypedValue;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.List;
public class EmojiTextView extends AppCompatTextView {
@@ -35,6 +45,9 @@ public class EmojiTextView extends AppCompatTextView {
private int maxLength;
private CharSequence overflowText;
private CharSequence previousOverflowText;
private boolean renderMentions;
private MentionRendererDelegate mentionRendererDelegate;
public EmojiTextView(Context context) {
this(context, null);
@@ -48,14 +61,33 @@ public class EmojiTextView extends AppCompatTextView {
super(context, attrs, defStyleAttr);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
scaleEmojis = a.getBoolean(R.styleable.EmojiTextView_scaleEmojis, false);
maxLength = a.getInteger(R.styleable.EmojiTextView_emoji_maxLength, -1);
forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
renderMentions = a.getBoolean(R.styleable.EmojiTextView_emoji_renderMentions, true);
a.recycle();
a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.textSize});
originalFontSize = a.getDimensionPixelSize(0, 0);
a.recycle();
if (renderMentions) {
mentionRendererDelegate = new MentionRendererDelegate(getContext(), ContextCompat.getColor(getContext(), R.color.transparent_black_20));
}
}
@Override
protected void onDraw(Canvas canvas) {
if (renderMentions && getText() instanceof Spanned && getLayout() != null) {
int checkpoint = canvas.save();
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
try {
mentionRendererDelegate.draw(canvas, (Spanned) getText(), getLayout());
} finally {
canvas.restoreToCount(checkpoint);
}
}
super.onDraw(canvas);
}
@Override public void setText(@Nullable CharSequence text, BufferType type) {
@@ -115,7 +147,19 @@ public class EmojiTextView extends AppCompatTextView {
private void ellipsizeAnyTextForMaxLength() {
if (maxLength > 0 && getText().length() > maxLength + 1) {
SpannableStringBuilder newContent = new SpannableStringBuilder();
newContent.append(getText().subSequence(0, maxLength)).append(ELLIPSIS).append(Optional.fromNullable(overflowText).or(""));
CharSequence shortenedText = getText().subSequence(0, maxLength);
if (shortenedText instanceof Spanned) {
Spanned spanned = (Spanned) shortenedText;
List<Annotation> mentionAnnotations = MentionAnnotation.getMentionAnnotations(spanned, maxLength - 1, maxLength);
if (!mentionAnnotations.isEmpty()) {
shortenedText = shortenedText.subSequence(0, spanned.getSpanStart(mentionAnnotations.get(0)));
}
}
newContent.append(shortenedText)
.append(ELLIPSIS)
.append(Util.emptyIfNull(overflowText));
EmojiParser.CandidateList newCandidates = EmojiProvider.getInstance(getContext()).getCandidates(newContent);

View File

@@ -2,16 +2,26 @@ package org.thoughtcrime.securesms.components.mention;
import android.text.Annotation;
import android.text.Spannable;
import android.text.Spanned;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.UUID;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.Collections;
import java.util.List;
/**
* Factory for creating mention annotation spans.
* This wraps an Android standard {@link Annotation} so it can leverage the built in
* span parceling for copy/paste. The annotation span contains the mentioned recipient's
* id (in numerical form).
*
* Note: This wraps creating an Android standard {@link Annotation} so it can leverage the built in
* span parceling for copy/paste. Do not extend Annotation or this will be lost.
* Note: Do not extend Annotation or the parceling behavior will be lost.
*/
public final class MentionAnnotation {
@@ -20,7 +30,45 @@ public final class MentionAnnotation {
private MentionAnnotation() {
}
public static Annotation mentionAnnotationForUuid(@NonNull UUID uuid) {
return new Annotation(MENTION_ANNOTATION, uuid.toString());
public static Annotation mentionAnnotationForRecipientId(@NonNull RecipientId id) {
return new Annotation(MENTION_ANNOTATION, idToMentionAnnotationValue(id));
}
public static String idToMentionAnnotationValue(@NonNull RecipientId id) {
return String.valueOf(id.toLong());
}
public static boolean isMentionAnnotation(@NonNull Annotation annotation) {
return MENTION_ANNOTATION.equals(annotation.getKey());
}
public static void setMentionAnnotations(Spannable body, List<Mention> mentions) {
for (Mention mention : mentions) {
body.setSpan(MentionAnnotation.mentionAnnotationForRecipientId(mention.getRecipientId()), mention.getStart(), mention.getStart() + mention.getLength(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
public static @NonNull List<Mention> getMentionsFromAnnotations(@Nullable CharSequence text) {
if (text instanceof Spanned) {
Spanned spanned = (Spanned) text;
return Stream.of(getMentionAnnotations(spanned))
.map(annotation -> {
int spanStart = spanned.getSpanStart(annotation);
int spanLength = spanned.getSpanEnd(annotation) - spanStart;
return new Mention(RecipientId.from(annotation.getValue()), spanStart, spanLength);
})
.toList();
}
return Collections.emptyList();
}
public static @NonNull List<Annotation> getMentionAnnotations(@NonNull Spanned spanned) {
return getMentionAnnotations(spanned, 0, spanned.length());
}
public static @NonNull List<Annotation> getMentionAnnotations(@NonNull Spanned spanned, int start, int end) {
return Stream.of(spanned.getSpans(start, end, Annotation.class))
.filter(MentionAnnotation::isMentionAnnotation)
.toList();
}
}

View File

@@ -0,0 +1,50 @@
package org.thoughtcrime.securesms.components.mention;
import android.text.Annotation;
import android.text.Editable;
import android.text.Spanned;
import android.text.TextWatcher;
import androidx.annotation.Nullable;
import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER;
/**
* Detects if some part of the mention is being deleted, and if so, deletes the entire mention and
* span from the text view.
*/
public class MentionDeleter implements TextWatcher {
@Nullable private Annotation toDelete;
@Override
public void beforeTextChanged(CharSequence sequence, int start, int count, int after) {
if (count > 0 && sequence instanceof Spanned) {
Spanned text = (Spanned) sequence;
for (Annotation annotation : MentionAnnotation.getMentionAnnotations(text, start, start + count)) {
if (text.getSpanStart(annotation) < start && text.getSpanEnd(annotation) > start) {
toDelete = annotation;
return;
}
}
}
}
@Override
public void afterTextChanged(Editable editable) {
if (toDelete == null) {
return;
}
int toDeleteStart = editable.getSpanStart(toDelete);
int toDeleteEnd = editable.getSpanEnd(toDelete);
editable.removeSpan(toDelete);
toDelete = null;
editable.replace(toDeleteStart, toDeleteEnd, String.valueOf(MENTION_STARTER));
}
@Override
public void onTextChanged(CharSequence sequence, int start, int before, int count) { }
}

View File

@@ -9,11 +9,11 @@ import android.text.Spanned;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Px;
import androidx.core.content.ContextCompat;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.DrawableUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
import org.thoughtcrime.securesms.util.ViewUtil;
/**
@@ -28,42 +28,32 @@ public class MentionRendererDelegate {
private final MentionRenderer multi;
private final int horizontalPadding;
public MentionRendererDelegate(@NonNull Context context) {
//noinspection ConstantConditions
this(ViewUtil.dpToPx(2),
ViewUtil.dpToPx(2),
ContextCompat.getDrawable(context, R.drawable.mention_text_bg),
ContextCompat.getDrawable(context, R.drawable.mention_text_bg_left),
ContextCompat.getDrawable(context, R.drawable.mention_text_bg_mid),
ContextCompat.getDrawable(context, R.drawable.mention_text_bg_right),
ThemeUtil.getThemedColor(context, R.attr.conversation_mention_background_color));
}
public MentionRendererDelegate(@NonNull Context context, @ColorInt int tint) {
this.horizontalPadding = ViewUtil.dpToPx(2);
public MentionRendererDelegate(int horizontalPadding,
int verticalPadding,
@NonNull Drawable drawable,
@NonNull Drawable drawableLeft,
@NonNull Drawable drawableMid,
@NonNull Drawable drawableEnd,
@ColorInt int tint)
{
this.horizontalPadding = horizontalPadding;
single = new MentionRenderer.SingleLineMentionRenderer(horizontalPadding,
verticalPadding,
DrawableUtil.tint(drawable, tint));
multi = new MentionRenderer.MultiLineMentionRenderer(horizontalPadding,
verticalPadding,
DrawableUtil.tint(drawableLeft, tint),
DrawableUtil.tint(drawableMid, tint),
DrawableUtil.tint(drawableEnd, tint));
Drawable drawable = ContextCompat.getDrawable(context, R.drawable.mention_text_bg);
Drawable drawableLeft = ContextCompat.getDrawable(context, R.drawable.mention_text_bg_left);
Drawable drawableMid = ContextCompat.getDrawable(context, R.drawable.mention_text_bg_mid);
Drawable drawableEnd = ContextCompat.getDrawable(context, R.drawable.mention_text_bg_right);
//noinspection ConstantConditions
single = new MentionRenderer.SingleLineMentionRenderer(horizontalPadding,
0,
DrawableUtil.tint(drawable, tint));
//noinspection ConstantConditions
multi = new MentionRenderer.MultiLineMentionRenderer(horizontalPadding,
0,
DrawableUtil.tint(drawableLeft, tint),
DrawableUtil.tint(drawableMid, tint),
DrawableUtil.tint(drawableEnd, tint));
}
public void draw(@NonNull Canvas canvas, @NonNull Spanned text, @NonNull Layout layout) {
Annotation[] spans = text.getSpans(0, text.length(), Annotation.class);
for (Annotation span : spans) {
if (MentionAnnotation.MENTION_ANNOTATION.equals(span.getKey())) {
int spanStart = text.getSpanStart(span);
int spanEnd = text.getSpanEnd(span);
Annotation[] annotations = text.getSpans(0, text.length(), Annotation.class);
for (Annotation annotation : annotations) {
if (MentionAnnotation.isMentionAnnotation(annotation)) {
int spanStart = text.getSpanStart(annotation);
int spanEnd = text.getSpanEnd(annotation);
int startLine = layout.getLineForOffset(spanStart);
int endLine = layout.getLineForOffset(spanEnd);

View File

@@ -0,0 +1,58 @@
package org.thoughtcrime.securesms.components.mention;
import android.text.Annotation;
import android.text.Editable;
import android.text.Spanned;
import android.text.TextWatcher;
import androidx.annotation.Nullable;
import java.util.List;
/**
* Provides a mechanism to validate mention annotations set on an edit text. This enables
* removing invalid mentions if the user mentioned isn't in the group.
*/
public class MentionValidatorWatcher implements TextWatcher {
@Nullable private List<Annotation> invalidMentionAnnotations;
@Nullable private MentionValidator mentionValidator;
@Override
public void onTextChanged(CharSequence sequence, int start, int before, int count) {
if (count > 1 && mentionValidator != null && sequence instanceof Spanned) {
Spanned span = (Spanned) sequence;
List<Annotation> mentionAnnotations = MentionAnnotation.getMentionAnnotations(span, start, start + count);
if (mentionAnnotations.size() > 0) {
invalidMentionAnnotations = mentionValidator.getInvalidMentionAnnotations(mentionAnnotations);
}
}
}
@Override
public void afterTextChanged(Editable editable) {
if (invalidMentionAnnotations == null) {
return;
}
List<Annotation> invalidMentions = invalidMentionAnnotations;
invalidMentionAnnotations = null;
for (Annotation annotation : invalidMentions) {
editable.removeSpan(annotation);
}
}
public void setMentionValidator(@Nullable MentionValidator mentionValidator) {
this.mentionValidator = mentionValidator;
}
@Override
public void beforeTextChanged(CharSequence sequence, int start, int count, int after) { }
public interface MentionValidator {
List<Annotation> getInvalidMentionAnnotations(List<Annotation> mentionAnnotations);
}
}