Add initial Mentions UI/UX for picker and compose edit.
This commit is contained in:
parent
8e45a546c9
commit
1ab61beeb9
28 changed files with 1019 additions and 16 deletions
|
@ -2,40 +2,50 @@ package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
|
import android.graphics.Canvas;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
import android.text.Editable;
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.annotation.RequiresApi;
|
|
||||||
import androidx.core.view.inputmethod.EditorInfoCompat;
|
|
||||||
import androidx.core.view.inputmethod.InputConnectionCompat;
|
|
||||||
import androidx.core.view.inputmethod.InputContentInfoCompat;
|
|
||||||
import androidx.core.os.BuildCompat;
|
|
||||||
|
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.text.Spannable;
|
import android.text.Spannable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.TextUtils.TruncateAt;
|
import android.text.TextUtils.TruncateAt;
|
||||||
|
import android.text.method.QwertyKeyListener;
|
||||||
import android.text.style.RelativeSizeSpan;
|
import android.text.style.RelativeSizeSpan;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.view.inputmethod.InputConnection;
|
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;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.TransportOption;
|
import org.thoughtcrime.securesms.TransportOption;
|
||||||
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
||||||
|
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||||
|
import org.thoughtcrime.securesms.components.mention.MentionRendererDelegate;
|
||||||
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public class ComposeText extends EmojiEditText {
|
public class ComposeText extends EmojiEditText {
|
||||||
|
|
||||||
private CharSequence hint;
|
private CharSequence hint;
|
||||||
private SpannableString subHint;
|
private SpannableString subHint;
|
||||||
|
private MentionRendererDelegate mentionRendererDelegate;
|
||||||
|
|
||||||
@Nullable private InputPanel.MediaListener mediaListener;
|
@Nullable private InputPanel.MediaListener mediaListener;
|
||||||
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
|
@Nullable private CursorPositionChangedListener cursorPositionChangedListener;
|
||||||
|
@Nullable private MentionQueryChangedListener mentionQueryChangedListener;
|
||||||
|
|
||||||
public ComposeText(Context context) {
|
public ComposeText(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
|
@ -75,11 +85,33 @@ public class ComposeText extends EmojiEditText {
|
||||||
protected void onSelectionChanged(int selStart, int selEnd) {
|
protected void onSelectionChanged(int selStart, int selEnd) {
|
||||||
super.onSelectionChanged(selStart, selEnd);
|
super.onSelectionChanged(selStart, selEnd);
|
||||||
|
|
||||||
|
if (FeatureFlags.mentions()) {
|
||||||
|
if (selStart == selEnd) {
|
||||||
|
doAfterCursorChange();
|
||||||
|
} else {
|
||||||
|
updateQuery("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (cursorPositionChangedListener != null) {
|
if (cursorPositionChangedListener != null) {
|
||||||
cursorPositionChangedListener.onCursorPositionChanged(selStart, selEnd);
|
cursorPositionChangedListener.onCursorPositionChanged(selStart, selEnd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
if (FeatureFlags.mentions() && getText() != null && getLayout() != null) {
|
||||||
|
int checkpoint = canvas.save();
|
||||||
|
canvas.translate(getTotalPaddingLeft(), getTotalPaddingTop());
|
||||||
|
try {
|
||||||
|
mentionRendererDelegate.draw(canvas, getText(), getLayout());
|
||||||
|
} finally {
|
||||||
|
canvas.restoreToCount(checkpoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
super.onDraw(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
private CharSequence ellipsizeToWidth(CharSequence text) {
|
private CharSequence ellipsizeToWidth(CharSequence text) {
|
||||||
return TextUtils.ellipsize(text,
|
return TextUtils.ellipsize(text,
|
||||||
getPaint(),
|
getPaint(),
|
||||||
|
@ -119,6 +151,10 @@ public class ComposeText extends EmojiEditText {
|
||||||
this.cursorPositionChangedListener = listener;
|
this.cursorPositionChangedListener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setMentionQueryChangedListener(@Nullable MentionQueryChangedListener listener) {
|
||||||
|
this.mentionQueryChangedListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
private boolean isLandscape() {
|
private boolean isLandscape() {
|
||||||
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
|
return getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE;
|
||||||
}
|
}
|
||||||
|
@ -169,9 +205,89 @@ public class ComposeText extends EmojiEditText {
|
||||||
if (TextSecurePreferences.isIncognitoKeyboardEnabled(getContext())) {
|
if (TextSecurePreferences.isIncognitoKeyboardEnabled(getContext())) {
|
||||||
setImeOptions(getImeOptions() | 16777216);
|
setImeOptions(getImeOptions() | 16777216);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (FeatureFlags.mentions()) {
|
||||||
|
mentionRendererDelegate = new MentionRendererDelegate(getContext());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doAfterCursorChange() {
|
||||||
|
Editable text = getText();
|
||||||
|
if (text != null && enoughToFilter(text)) {
|
||||||
|
performFiltering(text);
|
||||||
|
} else {
|
||||||
|
updateQuery("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void performFiltering(@NonNull Editable text) {
|
||||||
|
int end = getSelectionEnd();
|
||||||
|
int start = findQueryStart(text, end);
|
||||||
|
CharSequence query = text.subSequence(start, end);
|
||||||
|
updateQuery(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateQuery(@NonNull CharSequence query) {
|
||||||
|
if (mentionQueryChangedListener != null) {
|
||||||
|
mentionQueryChangedListener.onQueryChanged(query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean enoughToFilter(@NonNull Editable text) {
|
||||||
|
int end = getSelectionEnd();
|
||||||
|
if (end < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return end - findQueryStart(text, end) >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void replaceTextWithMention(@NonNull String displayName, @NonNull UUID uuid) {
|
||||||
|
Editable text = getText();
|
||||||
|
if (text == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearComposingText();
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull CharSequence createReplacementToken(@NonNull CharSequence text, @NonNull UUID uuid) {
|
||||||
|
SpannableStringBuilder builder = new SpannableStringBuilder("@");
|
||||||
|
if (text instanceof Spanned) {
|
||||||
|
SpannableString spannableString = new SpannableString(text + " ");
|
||||||
|
TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, spannableString, 0);
|
||||||
|
builder.append(spannableString);
|
||||||
|
} else {
|
||||||
|
builder.append(text).append(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setSpan(MentionAnnotation.mentionAnnotationForUuid(uuid), 0, builder.length() - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int findQueryStart(@NonNull CharSequence text, int inputCursorPosition) {
|
||||||
|
if (inputCursorPosition == 0) {
|
||||||
|
return inputCursorPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
int delimiterSearchIndex = inputCursorPosition - 1;
|
||||||
|
while (delimiterSearchIndex >= 0 && (text.charAt(delimiterSearchIndex) != '@' && text.charAt(delimiterSearchIndex) != ' ')) {
|
||||||
|
delimiterSearchIndex--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delimiterSearchIndex >= 0 && text.charAt(delimiterSearchIndex) == '@') {
|
||||||
|
return delimiterSearchIndex + 1;
|
||||||
|
}
|
||||||
|
return inputCursorPosition;
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.HONEYCOMB_MR2)
|
|
||||||
private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
|
private static class CommitContentListener implements InputConnectionCompat.OnCommitContentListener {
|
||||||
|
|
||||||
private static final String TAG = CommitContentListener.class.getSimpleName();
|
private static final String TAG = CommitContentListener.class.getSimpleName();
|
||||||
|
@ -207,4 +323,8 @@ public class ComposeText extends EmojiEditText {
|
||||||
public interface CursorPositionChangedListener {
|
public interface CursorPositionChangedListener {
|
||||||
void onCursorPositionChanged(int start, int end);
|
void onCursorPositionChanged(int start, int end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface MentionQueryChangedListener {
|
||||||
|
void onQueryChanged(CharSequence query);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
package org.thoughtcrime.securesms.components.mention;
|
||||||
|
|
||||||
|
|
||||||
|
import android.text.Annotation;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory for creating mention annotation spans.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
public final class MentionAnnotation {
|
||||||
|
|
||||||
|
public static final String MENTION_ANNOTATION = "mention";
|
||||||
|
|
||||||
|
private MentionAnnotation() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Annotation mentionAnnotationForUuid(@NonNull UUID uuid) {
|
||||||
|
return new Annotation(MENTION_ANNOTATION, uuid.toString());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
package org.thoughtcrime.securesms.components.mention;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.text.Layout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.util.LayoutUtil;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles actually drawing the mention backgrounds for a TextView.
|
||||||
|
* <p>
|
||||||
|
* Ported and modified from https://github.com/googlearchive/android-text/tree/master/RoundedBackground-Kotlin
|
||||||
|
*/
|
||||||
|
public abstract class MentionRenderer {
|
||||||
|
|
||||||
|
protected final int horizontalPadding;
|
||||||
|
protected final int verticalPadding;
|
||||||
|
|
||||||
|
public MentionRenderer(int horizontalPadding, int verticalPadding) {
|
||||||
|
this.horizontalPadding = horizontalPadding;
|
||||||
|
this.verticalPadding = verticalPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void draw(@NonNull Canvas canvas, @NonNull Layout layout, int startLine, int endLine, int startOffset, int endOffset);
|
||||||
|
|
||||||
|
protected int getLineTop(@NonNull Layout layout, int line) {
|
||||||
|
return LayoutUtil.getLineTopWithoutPadding(layout, line) - verticalPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected int getLineBottom(@NonNull Layout layout, int line) {
|
||||||
|
return LayoutUtil.getLineBottomWithoutPadding(layout, line) + verticalPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class SingleLineMentionRenderer extends MentionRenderer {
|
||||||
|
|
||||||
|
private final Drawable drawable;
|
||||||
|
|
||||||
|
public SingleLineMentionRenderer(int horizontalPadding, int verticalPadding, @NonNull Drawable drawable) {
|
||||||
|
super(horizontalPadding, verticalPadding);
|
||||||
|
this.drawable = drawable;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(@NonNull Canvas canvas, @NonNull Layout layout, int startLine, int endLine, int startOffset, int endOffset) {
|
||||||
|
int lineTop = getLineTop(layout, startLine);
|
||||||
|
int lineBottom = getLineBottom(layout, startLine);
|
||||||
|
int left = Math.min(startOffset, endOffset);
|
||||||
|
int right = Math.max(startOffset, endOffset);
|
||||||
|
|
||||||
|
drawable.setBounds(left, lineTop, right, lineBottom);
|
||||||
|
drawable.draw(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class MultiLineMentionRenderer extends MentionRenderer {
|
||||||
|
|
||||||
|
private final Drawable drawableLeft;
|
||||||
|
private final Drawable drawableMid;
|
||||||
|
private final Drawable drawableRight;
|
||||||
|
|
||||||
|
public MultiLineMentionRenderer(int horizontalPadding, int verticalPadding,
|
||||||
|
@NonNull Drawable drawableLeft,
|
||||||
|
@NonNull Drawable drawableMid,
|
||||||
|
@NonNull Drawable drawableRight)
|
||||||
|
{
|
||||||
|
super(horizontalPadding, verticalPadding);
|
||||||
|
this.drawableLeft = drawableLeft;
|
||||||
|
this.drawableMid = drawableMid;
|
||||||
|
this.drawableRight = drawableRight;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(@NonNull Canvas canvas, @NonNull Layout layout, int startLine, int endLine, int startOffset, int endOffset) {
|
||||||
|
int paragraphDirection = layout.getParagraphDirection(startLine);
|
||||||
|
|
||||||
|
float lineEndOffset;
|
||||||
|
if (paragraphDirection == Layout.DIR_RIGHT_TO_LEFT) {
|
||||||
|
lineEndOffset = layout.getLineLeft(startLine) - horizontalPadding;
|
||||||
|
} else {
|
||||||
|
lineEndOffset = layout.getLineRight(startLine) + horizontalPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
int lineBottom = getLineBottom(layout, startLine);
|
||||||
|
int lineTop = getLineTop(layout, startLine);
|
||||||
|
drawStart(canvas, startOffset, lineTop, (int) lineEndOffset, lineBottom);
|
||||||
|
|
||||||
|
for (int line = startLine + 1; line < endLine; line++) {
|
||||||
|
int left = (int) layout.getLineLeft(line) - horizontalPadding;
|
||||||
|
int right = (int) layout.getLineRight(line) + horizontalPadding;
|
||||||
|
|
||||||
|
lineTop = getLineTop(layout, line);
|
||||||
|
lineBottom = getLineBottom(layout, line);
|
||||||
|
|
||||||
|
drawableMid.setBounds(left, lineTop, right, lineBottom);
|
||||||
|
drawableMid.draw(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
float lineStartOffset;
|
||||||
|
if (paragraphDirection == Layout.DIR_RIGHT_TO_LEFT) {
|
||||||
|
lineStartOffset = layout.getLineRight(startLine) + horizontalPadding;
|
||||||
|
} else {
|
||||||
|
lineStartOffset = layout.getLineLeft(startLine) - horizontalPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineBottom = getLineBottom(layout, endLine);
|
||||||
|
lineTop = getLineTop(layout, endLine);
|
||||||
|
|
||||||
|
drawEnd(canvas, (int) lineStartOffset, lineTop, endOffset, lineBottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawStart(@NonNull Canvas canvas, int start, int top, int end, int bottom) {
|
||||||
|
if (start > end) {
|
||||||
|
drawableRight.setBounds(end, top, start, bottom);
|
||||||
|
drawableRight.draw(canvas);
|
||||||
|
} else {
|
||||||
|
drawableLeft.setBounds(start, top, end, bottom);
|
||||||
|
drawableLeft.draw(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawEnd(@NonNull Canvas canvas, int start, int top, int end, int bottom) {
|
||||||
|
if (start > end) {
|
||||||
|
drawableLeft.setBounds(end, top, start, bottom);
|
||||||
|
drawableLeft.draw(canvas);
|
||||||
|
} else {
|
||||||
|
drawableRight.setBounds(start, top, end, bottom);
|
||||||
|
drawableRight.draw(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package org.thoughtcrime.securesms.components.mention;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.text.Annotation;
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.Spanned;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates the logic for determining the type of mention rendering needed (single vs multi-line) and then
|
||||||
|
* passing that information to the appropriate {@link MentionRenderer}.
|
||||||
|
* <p></p>
|
||||||
|
* Ported and modified from https://github.com/googlearchive/android-text/tree/master/RoundedBackground-Kotlin
|
||||||
|
*/
|
||||||
|
public class MentionRendererDelegate {
|
||||||
|
|
||||||
|
private final MentionRenderer single;
|
||||||
|
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(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));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
int startLine = layout.getLineForOffset(spanStart);
|
||||||
|
int endLine = layout.getLineForOffset(spanEnd);
|
||||||
|
|
||||||
|
int startOffset = (int) (layout.getPrimaryHorizontal(spanStart) + -1 * layout.getParagraphDirection(startLine) * horizontalPadding);
|
||||||
|
int endOffset = (int) (layout.getPrimaryHorizontal(spanEnd) + layout.getParagraphDirection(endLine) * horizontalPadding);
|
||||||
|
|
||||||
|
MentionRenderer renderer = (startLine == endLine) ? single : multi;
|
||||||
|
renderer.draw(canvas, layout, startLine, endLine, startOffset, endOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -125,6 +125,7 @@ import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||||
import org.thoughtcrime.securesms.conversation.ConversationGroupViewModel.GroupActiveState;
|
import org.thoughtcrime.securesms.conversation.ConversationGroupViewModel.GroupActiveState;
|
||||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
|
||||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
|
@ -226,6 +227,7 @@ import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
|
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.thoughtcrime.securesms.util.MessageUtil;
|
import org.thoughtcrime.securesms.util.MessageUtil;
|
||||||
|
@ -341,6 +343,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||||
private InputPanel inputPanel;
|
private InputPanel inputPanel;
|
||||||
private View panelParent;
|
private View panelParent;
|
||||||
private View noLongerMemberBanner;
|
private View noLongerMemberBanner;
|
||||||
|
private Stub<View> mentionsSuggestions;
|
||||||
|
|
||||||
private LinkPreviewViewModel linkPreviewViewModel;
|
private LinkPreviewViewModel linkPreviewViewModel;
|
||||||
private ConversationSearchViewModel searchViewModel;
|
private ConversationSearchViewModel searchViewModel;
|
||||||
|
@ -414,6 +417,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||||
initializeStickerObserver();
|
initializeStickerObserver();
|
||||||
initializeViewModel();
|
initializeViewModel();
|
||||||
initializeGroupViewModel();
|
initializeGroupViewModel();
|
||||||
|
if (FeatureFlags.mentions()) initializeMentionsViewModel();
|
||||||
initializeEnabledCheck();
|
initializeEnabledCheck();
|
||||||
initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
|
initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener<Boolean>() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -1700,6 +1704,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||||
searchNav = ViewUtil.findById(this, R.id.conversation_search_nav);
|
searchNav = ViewUtil.findById(this, R.id.conversation_search_nav);
|
||||||
messageRequestBottomView = ViewUtil.findById(this, R.id.conversation_activity_message_request_bottom_bar);
|
messageRequestBottomView = ViewUtil.findById(this, R.id.conversation_activity_message_request_bottom_bar);
|
||||||
reactionOverlay = ViewUtil.findById(this, R.id.conversation_reaction_scrubber);
|
reactionOverlay = ViewUtil.findById(this, R.id.conversation_reaction_scrubber);
|
||||||
|
mentionsSuggestions = ViewUtil.findStubById(this, R.id.conversation_mention_suggestions_stub);
|
||||||
|
|
||||||
ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
|
ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
|
||||||
ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button);
|
ImageButton inlineAttachmentButton = ViewUtil.findById(this, R.id.inline_attachment_button);
|
||||||
|
@ -1864,6 +1869,28 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
||||||
groupViewModel.getGroupActiveState().observe(this, unused -> invalidateOptionsMenu());
|
groupViewModel.getGroupActiveState().observe(this, unused -> invalidateOptionsMenu());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void initializeMentionsViewModel() {
|
||||||
|
MentionsPickerViewModel mentionsViewModel = ViewModelProviders.of(this, new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
|
||||||
|
|
||||||
|
recipient.observe(this, mentionsViewModel::onRecipientChange);
|
||||||
|
composeText.setMentionQueryChangedListener(query -> {
|
||||||
|
if (getRecipient().isGroup()) {
|
||||||
|
if (!mentionsSuggestions.resolved()) {
|
||||||
|
mentionsSuggestions.get();
|
||||||
|
}
|
||||||
|
mentionsViewModel.onQueryChange(query);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mentionsViewModel.getSelectedRecipient().observe(this, recipient -> {
|
||||||
|
String replacementDisplayName = recipient.getDisplayName(this);
|
||||||
|
if (replacementDisplayName.equals(recipient.getDisplayUsername())) {
|
||||||
|
replacementDisplayName = recipient.getUsername().or(replacementDisplayName);
|
||||||
|
}
|
||||||
|
composeText.replaceTextWithMention(replacementDisplayName, recipient.requireUuid());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void showStickerIntroductionTooltip() {
|
private void showStickerIntroductionTooltip() {
|
||||||
TextSecurePreferences.setMediaKeyboardMode(this, MediaKeyboardMode.STICKER);
|
TextSecurePreferences.setMediaKeyboardMode(this, MediaKeyboardMode.STICKER);
|
||||||
inputPanel.setMediaKeyboardToggleMode(true);
|
inputPanel.setMediaKeyboardToggleMode(true);
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||||
|
import org.thoughtcrime.securesms.util.MappingViewHolder;
|
||||||
|
|
||||||
|
public class MentionViewHolder extends MappingViewHolder<MentionViewState> {
|
||||||
|
|
||||||
|
private final AvatarImageView avatar;
|
||||||
|
private final TextView name;
|
||||||
|
@Nullable private final MentionEventsListener mentionEventsListener;
|
||||||
|
|
||||||
|
public MentionViewHolder(@NonNull View itemView, @Nullable MentionEventsListener mentionEventsListener) {
|
||||||
|
super(itemView);
|
||||||
|
this.mentionEventsListener = mentionEventsListener;
|
||||||
|
|
||||||
|
avatar = findViewById(R.id.mention_recipient_avatar);
|
||||||
|
name = findViewById(R.id.mention_recipient_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bind(@NonNull MentionViewState model) {
|
||||||
|
avatar.setRecipient(model.getRecipient());
|
||||||
|
name.setText(model.getName(context));
|
||||||
|
itemView.setOnClickListener(v -> {
|
||||||
|
if (mentionEventsListener != null) {
|
||||||
|
mentionEventsListener.onMentionClicked(model.getRecipient());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface MentionEventsListener {
|
||||||
|
void onMentionClicked(@NonNull Recipient recipient);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MappingAdapter.Factory<MentionViewState> createFactory(@Nullable MentionEventsListener mentionEventsListener) {
|
||||||
|
return new MappingAdapter.LayoutFactory<>(view -> new MentionViewHolder(view, mentionEventsListener), R.layout.mentions_recipient_list_item);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.util.MappingModel;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public final class MentionViewState implements MappingModel<MentionViewState> {
|
||||||
|
|
||||||
|
private final Recipient recipient;
|
||||||
|
|
||||||
|
public MentionViewState(@NonNull Recipient recipient) {
|
||||||
|
this.recipient = recipient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull String getName(@NonNull Context context) {
|
||||||
|
return recipient.getDisplayName(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull Recipient getRecipient() {
|
||||||
|
return recipient;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(@NonNull MentionViewState newItem) {
|
||||||
|
return recipient.getId().equals(newItem.recipient.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull MentionViewState newItem) {
|
||||||
|
Context context = ApplicationDependencies.getApplication();
|
||||||
|
return recipient.getDisplayName(context).equals(newItem.recipient.getDisplayName(context)) &&
|
||||||
|
Objects.equals(recipient.getProfileAvatar(), newItem.recipient.getProfileAvatar());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionViewHolder.MentionEventsListener;
|
||||||
|
import org.thoughtcrime.securesms.util.MappingAdapter;
|
||||||
|
|
||||||
|
public class MentionsPickerAdapter extends MappingAdapter {
|
||||||
|
public MentionsPickerAdapter(@Nullable MentionEventsListener mentionEventsListener) {
|
||||||
|
registerFactory(MentionViewState.class, MentionViewHolder.createFactory(mentionEventsListener));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.LoggingFragment;
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.util.MappingModel;
|
||||||
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MentionsPickerFragment extends LoggingFragment {
|
||||||
|
|
||||||
|
private MentionsPickerAdapter adapter;
|
||||||
|
private RecyclerView list;
|
||||||
|
private BottomSheetBehavior<View> behavior;
|
||||||
|
private MentionsPickerViewModel viewModel;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
|
View view = inflater.inflate(R.layout.mentions_picker_fragment, container, false);
|
||||||
|
|
||||||
|
list = view.findViewById(R.id.mentions_picker_list);
|
||||||
|
behavior = BottomSheetBehavior.from(view.findViewById(R.id.mentions_picker_bottom_sheet));
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
initializeList();
|
||||||
|
|
||||||
|
viewModel = ViewModelProviders.of(requireActivity()).get(MentionsPickerViewModel.class);
|
||||||
|
viewModel.getMentionList().observe(getViewLifecycleOwner(), this::updateList);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeList() {
|
||||||
|
adapter = new MentionsPickerAdapter(this::handleMentionClicked);
|
||||||
|
|
||||||
|
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(requireContext()) {
|
||||||
|
@Override
|
||||||
|
public void onLayoutCompleted(RecyclerView.State state) {
|
||||||
|
super.onLayoutCompleted(state);
|
||||||
|
updateBottomSheetBehavior(adapter.getItemCount());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
list.setLayoutManager(layoutManager);
|
||||||
|
list.setAdapter(adapter);
|
||||||
|
list.setItemAnimator(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleMentionClicked(@NonNull Recipient recipient) {
|
||||||
|
viewModel.onSelectionChange(recipient);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateList(@NonNull List<MappingModel<?>> mappingModels) {
|
||||||
|
adapter.submitList(mappingModels);
|
||||||
|
if (mappingModels.isEmpty()) {
|
||||||
|
updateBottomSheetBehavior(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateBottomSheetBehavior(int count) {
|
||||||
|
if (count > 0) {
|
||||||
|
if (behavior.getPeekHeight() == 0) {
|
||||||
|
behavior.setPeekHeight(ViewUtil.dpToPx(240), true);
|
||||||
|
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||||
|
behavior.setPeekHeight(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.Transformations;
|
||||||
|
import androidx.lifecycle.ViewModel;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
|
import com.annimon.stream.Stream;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||||
|
import org.thoughtcrime.securesms.groups.GroupId;
|
||||||
|
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||||
|
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry.FullMember;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
|
import org.thoughtcrime.securesms.util.MappingModel;
|
||||||
|
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||||
|
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class MentionsPickerViewModel extends ViewModel {
|
||||||
|
|
||||||
|
private final SingleLiveEvent<Recipient> selectedRecipient;
|
||||||
|
private final LiveData<List<MappingModel<?>>> mentionList;
|
||||||
|
private final MutableLiveData<LiveGroup> group;
|
||||||
|
private final MutableLiveData<CharSequence> liveQuery;
|
||||||
|
|
||||||
|
MentionsPickerViewModel() {
|
||||||
|
group = new MutableLiveData<>();
|
||||||
|
liveQuery = new MutableLiveData<>();
|
||||||
|
selectedRecipient = new SingleLiveEvent<>();
|
||||||
|
|
||||||
|
// TODO [cody] [mentions] simple query support implement for building UI/UX, to be replaced with better search before launch
|
||||||
|
LiveData<List<FullMember>> members = Transformations.distinctUntilChanged(Transformations.switchMap(group, LiveGroup::getFullMembers));
|
||||||
|
|
||||||
|
mentionList = LiveDataUtil.combineLatest(Transformations.distinctUntilChanged(liveQuery), members, this::filterMembers);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull LiveData<List<MappingModel<?>>> getMentionList() {
|
||||||
|
return mentionList;
|
||||||
|
}
|
||||||
|
|
||||||
|
void onSelectionChange(@NonNull Recipient recipient) {
|
||||||
|
selectedRecipient.setValue(recipient);
|
||||||
|
}
|
||||||
|
|
||||||
|
public @NonNull LiveData<Recipient> getSelectedRecipient() {
|
||||||
|
return selectedRecipient;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onQueryChange(@NonNull CharSequence query) {
|
||||||
|
liveQuery.setValue(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onRecipientChange(@NonNull Recipient recipient) {
|
||||||
|
GroupId groupId = recipient.getGroupId().orNull();
|
||||||
|
if (groupId != null) {
|
||||||
|
LiveGroup liveGroup = new LiveGroup(groupId);
|
||||||
|
group.setValue(liveGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NonNull List<MappingModel<?>> filterMembers(@NonNull CharSequence query, @NonNull List<FullMember> members) {
|
||||||
|
if (TextUtils.isEmpty(query)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Stream.of(members)
|
||||||
|
.filter(m -> m.getMember().getDisplayName(ApplicationDependencies.getApplication()).toLowerCase().replaceAll("\\s", "").startsWith(query.toString()))
|
||||||
|
.<MappingModel<?>>map(m -> new MentionViewState(m.getMember()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Factory implements ViewModelProvider.Factory {
|
||||||
|
@Override
|
||||||
|
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||||
|
//noinspection ConstantConditions
|
||||||
|
return modelClass.cast(new MentionsPickerViewModel());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -784,7 +784,7 @@ public class Recipient {
|
||||||
return ApplicationDependencies.getRecipientCache().getLive(id);
|
return ApplicationDependencies.getRecipientCache().getLive(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable String getDisplayUsername() {
|
public @Nullable String getDisplayUsername() {
|
||||||
if (!TextUtils.isEmpty(username)) {
|
if (!TextUtils.isEmpty(username)) {
|
||||||
return "@" + username;
|
return "@" + username;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -4,7 +4,9 @@ import android.graphics.Bitmap;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.graphics.drawable.DrawableCompat;
|
||||||
|
|
||||||
public final class DrawableUtil {
|
public final class DrawableUtil {
|
||||||
|
|
||||||
|
@ -19,4 +21,13 @@ public final class DrawableUtil {
|
||||||
|
|
||||||
return bitmap;
|
return bitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a new {@link Drawable} that safely wraps and tints the provided drawable.
|
||||||
|
*/
|
||||||
|
public static @NonNull Drawable tint(@NonNull Drawable drawable, @ColorInt int tint) {
|
||||||
|
Drawable tinted = DrawableCompat.wrap(drawable).mutate();
|
||||||
|
DrawableCompat.setTint(tinted, tint);
|
||||||
|
return tinted;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,7 @@ public final class FeatureFlags {
|
||||||
private static final String CDS = "android.cds";
|
private static final String CDS = "android.cds";
|
||||||
private static final String RECIPIENT_TRUST = "android.recipientTrust";
|
private static final String RECIPIENT_TRUST = "android.recipientTrust";
|
||||||
private static final String INTERNAL_USER = "android.internalUser";
|
private static final String INTERNAL_USER = "android.internalUser";
|
||||||
|
private static final String MENTIONS = "android.mentions";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||||
|
@ -71,7 +72,8 @@ public final class FeatureFlags {
|
||||||
GROUPS_V2_CREATE,
|
GROUPS_V2_CREATE,
|
||||||
GROUPS_V2_CAPACITY,
|
GROUPS_V2_CAPACITY,
|
||||||
RECIPIENT_TRUST,
|
RECIPIENT_TRUST,
|
||||||
INTERNAL_USER
|
INTERNAL_USER,
|
||||||
|
MENTIONS
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -221,6 +223,11 @@ public final class FeatureFlags {
|
||||||
return getBoolean(RECIPIENT_TRUST, false);
|
return getBoolean(RECIPIENT_TRUST, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether or not we allow mentions send support in groups. */
|
||||||
|
public static boolean mentions() {
|
||||||
|
return getBoolean(MENTIONS, false);
|
||||||
|
}
|
||||||
|
|
||||||
/** Only for rendering debug info. */
|
/** Only for rendering debug info. */
|
||||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||||
return new TreeMap<>(REMOTE_VALUES);
|
return new TreeMap<>(REMOTE_VALUES);
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import android.text.Layout;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility functions for dealing with {@link Layout}.
|
||||||
|
*
|
||||||
|
* Ported and modified from https://github.com/googlearchive/android-text/tree/master/RoundedBackground-Kotlin
|
||||||
|
*/
|
||||||
|
public class LayoutUtil {
|
||||||
|
private static final float DEFAULT_LINE_SPACING_EXTRA = 0f;
|
||||||
|
|
||||||
|
private static final float DEFAULT_LINE_SPACING_MULTIPLIER = 1f;
|
||||||
|
|
||||||
|
public static int getLineHeight(@NonNull Layout layout, int line) {
|
||||||
|
return layout.getLineTop(line + 1) - layout.getLineTop(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getLineTopWithoutPadding(@NonNull Layout layout, int line) {
|
||||||
|
int lineTop = layout.getLineTop(line);
|
||||||
|
if (line == 0) {
|
||||||
|
lineTop -= layout.getTopPadding();
|
||||||
|
}
|
||||||
|
return lineTop;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getLineBottomWithoutPadding(@NonNull Layout layout, int line) {
|
||||||
|
int lineBottom = getLineBottomWithoutSpacing(layout, line);
|
||||||
|
if (line == layout.getLineCount() - 1) {
|
||||||
|
lineBottom -= layout.getBottomPadding();
|
||||||
|
}
|
||||||
|
return lineBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getLineBottomWithoutSpacing(@NonNull Layout layout, int line) {
|
||||||
|
int lineBottom = layout.getLineBottom(line);
|
||||||
|
boolean isLastLine = line == layout.getLineCount() - 1;
|
||||||
|
float lineSpacingExtra = layout.getSpacingAdd();
|
||||||
|
float lineSpacingMultiplier = layout.getSpacingMultiplier();
|
||||||
|
boolean hasLineSpacing = lineSpacingExtra != DEFAULT_LINE_SPACING_EXTRA || lineSpacingMultiplier != DEFAULT_LINE_SPACING_MULTIPLIER;
|
||||||
|
|
||||||
|
int lineBottomWithoutSpacing;
|
||||||
|
if (!hasLineSpacing || isLastLine) {
|
||||||
|
lineBottomWithoutSpacing = lineBottom;
|
||||||
|
} else {
|
||||||
|
float extra;
|
||||||
|
if (Float.compare(lineSpacingMultiplier, DEFAULT_LINE_SPACING_MULTIPLIER) != 0) {
|
||||||
|
int lineHeight = getLineHeight(layout, line);
|
||||||
|
extra = lineHeight - (lineHeight - lineSpacingExtra) / lineSpacingMultiplier;
|
||||||
|
} else {
|
||||||
|
extra = lineSpacingExtra;
|
||||||
|
}
|
||||||
|
|
||||||
|
lineBottomWithoutSpacing = (int) (lineBottom - extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lineBottomWithoutSpacing;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.LayoutRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
|
import androidx.recyclerview.widget.ListAdapter;
|
||||||
|
|
||||||
|
import org.whispersystems.libsignal.util.guava.Function;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable and composable {@link androidx.recyclerview.widget.RecyclerView.Adapter} built on-top of {@link ListAdapter} to
|
||||||
|
* provide async item diffing support.
|
||||||
|
* <p></p>
|
||||||
|
* The adapter makes use of mapping a model class to view holder factory at runtime via one of the {@link #registerFactory(Class, Factory)}
|
||||||
|
* methods. The factory creates a view holder specifically designed to handle the paired model type. This allows the view holder concretely
|
||||||
|
* deal with the model type it cares about. Due to the enforcement of matching generics during factory registration we can safely ignore or
|
||||||
|
* override compiler typing recommendations when binding and diffing.
|
||||||
|
* <p></p>
|
||||||
|
* General pattern for implementation:
|
||||||
|
* <ol>
|
||||||
|
* <li>Create {@link MappingModel}s for the items in the list. These encapsulate data massaging methods for views to use and the diff logic.</li>
|
||||||
|
* <li>Create {@link MappingViewHolder}s for each item type in the list and their corresponding {@link Factory}.</li>
|
||||||
|
* <li>Create an instance or subclass of {@link MappingAdapter} and register the mapping of model type to view holder factory for that model type.</li>
|
||||||
|
* </ol>
|
||||||
|
* Event listeners, click or otherwise, are handled at the view holder level and should be passed into the appropriate view holder factories. This
|
||||||
|
* pattern mimics how we pass data into view models via factories.
|
||||||
|
* <p></p>
|
||||||
|
* NOTE: There can only be on factory registered per model type. Registering two for the same type will result in the last one being used. However, the
|
||||||
|
* same factory can be registered multiple times for multiple model types (if the model type class hierarchy supports it).
|
||||||
|
*/
|
||||||
|
public class MappingAdapter extends ListAdapter<MappingModel<?>, MappingViewHolder<?>> {
|
||||||
|
|
||||||
|
private final Map<Integer, Factory<?>> factories;
|
||||||
|
private final Map<Class<?>, Integer> itemTypes;
|
||||||
|
private int typeCount;
|
||||||
|
|
||||||
|
public MappingAdapter() {
|
||||||
|
super(new MappingDiffCallback());
|
||||||
|
|
||||||
|
factories = new HashMap<>();
|
||||||
|
itemTypes = new HashMap<>();
|
||||||
|
typeCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewAttachedToWindow(@NonNull MappingViewHolder<?> holder) {
|
||||||
|
super.onViewAttachedToWindow(holder);
|
||||||
|
holder.onAttachedToWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewDetachedFromWindow(@NonNull MappingViewHolder<?> holder) {
|
||||||
|
super.onViewDetachedFromWindow(holder);
|
||||||
|
holder.onDetachedFromWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends MappingModel<T>> void registerFactory(Class<T> clazz, Factory<T> factory) {
|
||||||
|
int type = typeCount++;
|
||||||
|
factories.put(type, factory);
|
||||||
|
itemTypes.put(clazz, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position) {
|
||||||
|
Integer type = itemTypes.get(getItem(position).getClass());
|
||||||
|
if (type != null) {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
throw new AssertionError("No view holder factory for type: " + getItem(position).getClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull MappingViewHolder<?> onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
return Objects.requireNonNull(factories.get(viewType)).createViewHolder(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull MappingViewHolder holder, int position) {
|
||||||
|
//noinspection unchecked
|
||||||
|
holder.bind(getItem(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class MappingDiffCallback extends DiffUtil.ItemCallback<MappingModel<?>> {
|
||||||
|
@Override
|
||||||
|
public boolean areItemsTheSame(@NonNull MappingModel oldItem, @NonNull MappingModel newItem) {
|
||||||
|
if (oldItem.getClass() == newItem.getClass()) {
|
||||||
|
//noinspection unchecked
|
||||||
|
return oldItem.areItemsTheSame(newItem);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("DiffUtilEquals")
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull MappingModel oldItem, @NonNull MappingModel newItem) {
|
||||||
|
if (oldItem.getClass() == newItem.getClass()) {
|
||||||
|
//noinspection unchecked
|
||||||
|
return oldItem.areContentsTheSame(newItem);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Factory<T extends MappingModel<T>> {
|
||||||
|
@NonNull MappingViewHolder<T> createViewHolder(ViewGroup parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LayoutFactory<T extends MappingModel<T>> implements Factory<T> {
|
||||||
|
private Function<View, MappingViewHolder<T>> creator;
|
||||||
|
private final int layout;
|
||||||
|
|
||||||
|
public LayoutFactory(Function<View, MappingViewHolder<T>> creator, @LayoutRes int layout) {
|
||||||
|
this.creator = creator;
|
||||||
|
this.layout = layout;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public @NonNull MappingViewHolder<T> createViewHolder(ViewGroup parent) {
|
||||||
|
return creator.apply(LayoutInflater.from(parent.getContext()).inflate(layout, parent, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
public interface MappingModel<T> {
|
||||||
|
boolean areItemsTheSame(@NonNull T newItem);
|
||||||
|
boolean areContentsTheSame(@NonNull T newItem);
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
import androidx.annotation.IdRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.lifecycle.Lifecycle;
|
||||||
|
import androidx.lifecycle.LifecycleOwner;
|
||||||
|
import androidx.lifecycle.LifecycleRegistry;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
public abstract class MappingViewHolder<Model extends MappingModel<Model>> extends LifecycleViewHolder implements LifecycleOwner {
|
||||||
|
|
||||||
|
protected final Context context;
|
||||||
|
|
||||||
|
public MappingViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
context = itemView.getContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T extends View> T findViewById(@IdRes int id) {
|
||||||
|
return itemView.findViewById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract void bind(@NonNull Model model);
|
||||||
|
}
|
5
app/src/main/res/drawable/mention_text_bg.xml
Normal file
5
app/src/main/res/drawable/mention_text_bg.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@color/black"/>
|
||||||
|
<corners android:radius="@dimen/mention_corner_radius"/>
|
||||||
|
</shape>
|
5
app/src/main/res/drawable/mention_text_bg_left.xml
Normal file
5
app/src/main/res/drawable/mention_text_bg_left.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@color/black"/>
|
||||||
|
<corners android:topLeftRadius="@dimen/mention_corner_radius" android:bottomLeftRadius="@dimen/mention_corner_radius"/>
|
||||||
|
</shape>
|
4
app/src/main/res/drawable/mention_text_bg_mid.xml
Normal file
4
app/src/main/res/drawable/mention_text_bg_mid.xml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@color/black"/>
|
||||||
|
</shape>
|
5
app/src/main/res/drawable/mention_text_bg_right.xml
Normal file
5
app/src/main/res/drawable/mention_text_bg_right.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<solid android:color="@color/black"/>
|
||||||
|
<corners android:topRightRadius="@dimen/mention_corner_radius" android:bottomRightRadius="@dimen/mention_corner_radius"/>
|
||||||
|
</shape>
|
|
@ -64,10 +64,22 @@
|
||||||
android:layout="@layout/conversation_activity_reminderview_stub" />
|
android:layout="@layout/conversation_activity_reminderview_stub" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/fragment_content"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1" />
|
android:layout_weight="1">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/fragment_content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<ViewStub
|
||||||
|
android:id="@+id/conversation_mention_suggestions_stub"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout="@layout/conversation_mention_suggestions_stub"/>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
<ViewStub
|
<ViewStub
|
||||||
android:id="@+id/attachment_editor_stub"
|
android:id="@+id/attachment_editor_stub"
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:tag="mentions_picker_fragment"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:name="org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerFragment"/>
|
29
app/src/main/res/layout/mentions_picker_fragment.xml
Normal file
29
app/src/main/res/layout/mentions_picker_fragment.xml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/mentions_picker_bottom_sheet"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:behavior_hideable="false"
|
||||||
|
app:behavior_peekHeight="0dp"
|
||||||
|
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="@color/core_grey_65"/>
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/mentions_picker_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:attr/windowBackground" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
28
app/src/main/res/layout/mentions_recipient_list_item.xml
Normal file
28
app/src/main/res/layout/mentions_recipient_list_item.xml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:minHeight="52dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:background="?selectableItemBackground">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||||
|
android:id="@+id/mention_recipient_avatar"
|
||||||
|
android:layout_width="28dp"
|
||||||
|
android:layout_height="28dp"
|
||||||
|
tools:src="@tools:sample/avatars" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/mention_recipient_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textAppearance="@style/Signal.Text.Preview"
|
||||||
|
tools:text="@tools:sample/full_names" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -86,6 +86,7 @@
|
||||||
<attr name="conversation_popup_theme" format="reference"/>
|
<attr name="conversation_popup_theme" format="reference"/>
|
||||||
<attr name="conversation_title_color" format="reference" />
|
<attr name="conversation_title_color" format="reference" />
|
||||||
<attr name="conversation_subtitle_color" format="reference" />
|
<attr name="conversation_subtitle_color" format="reference" />
|
||||||
|
<attr name="conversation_mention_background_color" format="reference" />
|
||||||
|
|
||||||
<attr name="emoji_tab_strip_background" format="color" />
|
<attr name="emoji_tab_strip_background" format="color" />
|
||||||
<attr name="emoji_tab_indicator" format="color" />
|
<attr name="emoji_tab_indicator" format="color" />
|
||||||
|
|
|
@ -78,6 +78,8 @@
|
||||||
<dimen name="quote_corner_radius_preview">18dp</dimen>
|
<dimen name="quote_corner_radius_preview">18dp</dimen>
|
||||||
<dimen name="quote_thumb_size">60dp</dimen>
|
<dimen name="quote_thumb_size">60dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="mention_corner_radius">4dp</dimen>
|
||||||
|
|
||||||
<integer name="media_overview_cols">3</integer>
|
<integer name="media_overview_cols">3</integer>
|
||||||
<dimen name="message_details_table_row_pad">8dp</dimen>
|
<dimen name="message_details_table_row_pad">8dp</dimen>
|
||||||
|
|
||||||
|
|
|
@ -259,6 +259,7 @@
|
||||||
<item name="conversation_popup_theme">@style/ThemeOverlay.AppCompat.Light</item>
|
<item name="conversation_popup_theme">@style/ThemeOverlay.AppCompat.Light</item>
|
||||||
<item name="conversation_title_color">@color/white</item>
|
<item name="conversation_title_color">@color/white</item>
|
||||||
<item name="conversation_subtitle_color">@color/transparent_white_90</item>
|
<item name="conversation_subtitle_color">@color/transparent_white_90</item>
|
||||||
|
<item name="conversation_mention_background_color">@color/core_grey_20</item>
|
||||||
|
|
||||||
<item name="safety_number_change_dialog_button_background">@color/core_grey_05</item>
|
<item name="safety_number_change_dialog_button_background">@color/core_grey_05</item>
|
||||||
<item name="safety_number_change_dialog_button_text_color">@color/core_ultramarine</item>
|
<item name="safety_number_change_dialog_button_text_color">@color/core_ultramarine</item>
|
||||||
|
@ -610,6 +611,7 @@
|
||||||
<item name="conversation_popup_theme">@style/ThemeOverlay.AppCompat.Dark</item>
|
<item name="conversation_popup_theme">@style/ThemeOverlay.AppCompat.Dark</item>
|
||||||
<item name="conversation_title_color">@color/transparent_white_90</item>
|
<item name="conversation_title_color">@color/transparent_white_90</item>
|
||||||
<item name="conversation_subtitle_color">@color/transparent_white_80</item>
|
<item name="conversation_subtitle_color">@color/transparent_white_80</item>
|
||||||
|
<item name="conversation_mention_background_color">@color/core_grey_75</item>
|
||||||
<item name="conversation_scroll_to_bottom_background">@drawable/scroll_to_bottom_background_dark</item>
|
<item name="conversation_scroll_to_bottom_background">@drawable/scroll_to_bottom_background_dark</item>
|
||||||
<item name="conversation_scroll_to_bottom_foreground_color">@color/core_white</item>
|
<item name="conversation_scroll_to_bottom_foreground_color">@color/core_white</item>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue