Add mentions for v2 group chats.
parent
0bb9c1d650
commit
b2d4c5d14b
|
@ -47,7 +47,7 @@ public interface BindableConversationItem extends Unbindable {
|
|||
void onMessageSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onInviteSharedContactClicked(@NonNull List<Recipient> choices);
|
||||
void onReactionClicked(@NonNull View reactionTarget, long messageId, boolean isMms);
|
||||
void onGroupMemberAvatarClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId);
|
||||
void onMessageWithErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) { }
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -41,6 +41,8 @@ import android.provider.Browser;
|
|||
import android.provider.ContactsContract;
|
||||
import android.provider.Telephony;
|
||||
import android.text.Editable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.Gravity;
|
||||
import android.view.KeyEvent;
|
||||
|
@ -72,6 +74,7 @@ import androidx.core.content.pm.ShortcutManagerCompat;
|
|||
import androidx.core.graphics.drawable.IconCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
|
@ -111,6 +114,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
|||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
|
||||
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.Reminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.ReminderView;
|
||||
|
@ -124,6 +128,7 @@ import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity;
|
|||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationGroupViewModel.GroupActiveState;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.SafetyNumberChangeDialog;
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
|
@ -136,12 +141,14 @@ import org.thoughtcrime.securesms.database.GroupDatabase;
|
|||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil.UpdatedBodyAndMentions;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.identity.IdentityRecordList;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
|
@ -196,7 +203,6 @@ import org.thoughtcrime.securesms.mms.Slide;
|
|||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.mms.StickerSlide;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.profiles.GroupShareProfileView;
|
||||
|
@ -221,6 +227,7 @@ import org.thoughtcrime.securesms.stickers.StickerLocator;
|
|||
import org.thoughtcrime.securesms.stickers.StickerManagementActivity;
|
||||
import org.thoughtcrime.securesms.stickers.StickerPackInstallEvent;
|
||||
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
|
@ -248,10 +255,12 @@ import org.whispersystems.libsignal.util.Pair;
|
|||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
@ -648,6 +657,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
boolean initiating = threadId == -1;
|
||||
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orNull();
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
List<Mention> mentions = new ArrayList<>(result.getMentions());
|
||||
|
||||
for (Media mediaItem : result.getNonUploadedMedia()) {
|
||||
if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
|
||||
|
@ -669,6 +679,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
quote,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
mentions,
|
||||
expiresIn,
|
||||
result.isViewOnce(),
|
||||
subscriptionId,
|
||||
|
@ -1373,7 +1384,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
private ListenableFuture<Boolean> initializeDraft() {
|
||||
final SettableFuture<Boolean> result = new SettableFuture<>();
|
||||
|
||||
final String draftText = getIntent().getStringExtra(TEXT_EXTRA);
|
||||
final CharSequence draftText = getIntent().getCharSequenceExtra(TEXT_EXTRA);
|
||||
final Uri draftMedia = getIntent().getData();
|
||||
final String draftContentType = getIntent().getType();
|
||||
final MediaType draftMediaType = MediaType.from(draftContentType);
|
||||
|
@ -1437,19 +1448,34 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
private ListenableFuture<Boolean> initializeDraftFromDatabase() {
|
||||
SettableFuture<Boolean> future = new SettableFuture<>();
|
||||
|
||||
new AsyncTask<Void, Void, List<Draft>>() {
|
||||
new AsyncTask<Void, Void, Pair<Drafts, CharSequence>>() {
|
||||
@Override
|
||||
protected List<Draft> doInBackground(Void... params) {
|
||||
DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(ConversationActivity.this);
|
||||
List<Draft> results = draftDatabase.getDrafts(threadId);
|
||||
protected Pair<Drafts, CharSequence> doInBackground(Void... params) {
|
||||
Context context = ConversationActivity.this;
|
||||
DraftDatabase draftDatabase = DatabaseFactory.getDraftDatabase(context);
|
||||
Drafts results = draftDatabase.getDrafts(threadId);
|
||||
Draft mentionsDraft = results.getDraftOfType(Draft.MENTION);
|
||||
Spannable updatedText = null;
|
||||
|
||||
if (mentionsDraft != null) {
|
||||
String text = results.getDraftOfType(Draft.TEXT).getValue();
|
||||
List<Mention> mentions = MentionUtil.bodyRangeListToMentions(context, Base64.decodeOrThrow(mentionsDraft.getValue()));
|
||||
UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, text, mentions);
|
||||
|
||||
updatedText = new SpannableString(updated.getBody());
|
||||
MentionAnnotation.setMentionAnnotations(updatedText, updated.getMentions());
|
||||
}
|
||||
|
||||
draftDatabase.clearDrafts(threadId);
|
||||
|
||||
return results;
|
||||
return new Pair<>(results, updatedText);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<Draft> drafts) {
|
||||
protected void onPostExecute(Pair<Drafts, CharSequence> draftsWithUpdatedMentions) {
|
||||
Drafts drafts = Objects.requireNonNull(draftsWithUpdatedMentions.first());
|
||||
CharSequence updatedText = draftsWithUpdatedMentions.second();
|
||||
|
||||
if (drafts.isEmpty()) {
|
||||
future.set(false);
|
||||
updateToggleButtonState();
|
||||
|
@ -1473,7 +1499,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
try {
|
||||
switch (draft.getType()) {
|
||||
case Draft.TEXT:
|
||||
composeText.setText(draft.getValue());
|
||||
composeText.setText(updatedText == null ? draft.getValue() : updatedText);
|
||||
listener.onSuccess(true);
|
||||
break;
|
||||
case Draft.LOCATION:
|
||||
|
@ -1874,8 +1900,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
MentionsPickerViewModel mentionsViewModel = ViewModelProviders.of(this, new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
|
||||
|
||||
recipient.observe(this, mentionsViewModel::onRecipientChange);
|
||||
|
||||
composeText.setMentionQueryChangedListener(query -> {
|
||||
if (getRecipient().isGroup()) {
|
||||
if (getRecipient().isPushV2Group()) {
|
||||
if (!mentionsSuggestions.resolved()) {
|
||||
mentionsSuggestions.get();
|
||||
}
|
||||
|
@ -1883,12 +1910,26 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
}
|
||||
});
|
||||
|
||||
composeText.setMentionValidator(annotations -> {
|
||||
if (!getRecipient().isPushV2Group()) {
|
||||
return annotations;
|
||||
}
|
||||
|
||||
Set<String> validRecipientIds = Stream.of(getRecipient().getParticipants())
|
||||
.map(r -> MentionAnnotation.idToMentionAnnotationValue(r.getId()))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return Stream.of(annotations)
|
||||
.filterNot(a -> validRecipientIds.contains(a.getValue()))
|
||||
.toList();
|
||||
});
|
||||
|
||||
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());
|
||||
composeText.replaceTextWithMention(replacementDisplayName, recipient.getId());
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -2073,7 +2114,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
boolean initiating = threadId == -1;
|
||||
|
||||
sendMediaMessage(isSmsForced(), "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), expiresIn, false, subscriptionId, initiating, false);
|
||||
sendMediaMessage(isSmsForced(), "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, false);
|
||||
}
|
||||
|
||||
private void selectContactInfo(ContactData contactData) {
|
||||
|
@ -2097,7 +2138,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
Drafts drafts = new Drafts();
|
||||
|
||||
if (!Util.isEmpty(composeText)) {
|
||||
drafts.add(new Draft(Draft.TEXT, composeText.getTextTrimmed()));
|
||||
drafts.add(new Draft(Draft.TEXT, composeText.getTextTrimmed().toString()));
|
||||
List<Mention> draftMentions = composeText.getMentions();
|
||||
if (!draftMentions.isEmpty()) {
|
||||
drafts.add(new Draft(Draft.MENTION, Base64.encodeBytes(MentionUtil.mentionsToBodyRangeList(draftMentions).toByteArray())));
|
||||
}
|
||||
}
|
||||
|
||||
for (Slide slide : attachmentManager.buildSlideDeck().getSlides()) {
|
||||
|
@ -2187,7 +2232,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
}
|
||||
|
||||
private void calculateCharactersRemaining() {
|
||||
String messageBody = composeText.getTextTrimmed();
|
||||
String messageBody = composeText.getTextTrimmed().toString();
|
||||
TransportOption transportOption = sendButton.getSelectedTransport();
|
||||
CharacterState characterState = transportOption.calculateCharacters(messageBody);
|
||||
|
||||
|
@ -2270,7 +2315,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
}
|
||||
|
||||
private String getMessage() throws InvalidMessageException {
|
||||
String rawText = composeText.getTextTrimmed();
|
||||
String rawText = composeText.getTextTrimmed().toString();
|
||||
|
||||
if (rawText.length() < 1 && !attachmentManager.isAttachmentPresent())
|
||||
throw new InvalidMessageException(getString(R.string.ConversationActivity_message_is_empty_exclamation));
|
||||
|
@ -2339,6 +2384,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
recipient.isGroup() ||
|
||||
recipient.getEmail().isPresent() ||
|
||||
inputPanel.getQuote().isPresent() ||
|
||||
composeText.hasMentions() ||
|
||||
linkPreviewViewModel.hasLinkPreview() ||
|
||||
needsSplit;
|
||||
|
||||
|
@ -2369,9 +2415,10 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
private void sendMediaMessage(@NonNull MediaSendActivityResult result) {
|
||||
long expiresIn = recipient.get().getExpireMessages() * 1000L;
|
||||
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orNull();
|
||||
List<Mention> mentions = new ArrayList<>(result.getMentions());
|
||||
boolean initiating = threadId == -1;
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, quote, Collections.emptyList(), Collections.emptyList());
|
||||
OutgoingMediaMessage secureMessage = new OutgoingSecureMediaMessage(message );
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient.get(), new SlideDeck(), result.getBody(), System.currentTimeMillis(), -1, expiresIn, result.isViewOnce(), distributionType, quote, Collections.emptyList(), Collections.emptyList(), mentions);
|
||||
OutgoingMediaMessage secureMessage = new OutgoingSecureMediaMessage(message);
|
||||
|
||||
ApplicationContext.getInstance(this).getTypingStatusSender().onTypingStopped(threadId);
|
||||
|
||||
|
@ -2395,7 +2442,18 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
throws InvalidMessageException
|
||||
{
|
||||
Log.i(TAG, "Sending media message...");
|
||||
sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck(), inputPanel.getQuote().orNull(), Collections.emptyList(), linkPreviewViewModel.getActiveLinkPreviews(), expiresIn, viewOnce, subscriptionId, initiating, true);
|
||||
sendMediaMessage(forceSms,
|
||||
getMessage(),
|
||||
attachmentManager.buildSlideDeck(),
|
||||
inputPanel.getQuote().orNull(),
|
||||
Collections.emptyList(),
|
||||
linkPreviewViewModel.getActiveLinkPreviews(),
|
||||
composeText.getMentions(),
|
||||
expiresIn,
|
||||
viewOnce,
|
||||
subscriptionId,
|
||||
initiating,
|
||||
true);
|
||||
}
|
||||
|
||||
private ListenableFuture<Void> sendMediaMessage(final boolean forceSms,
|
||||
|
@ -2404,6 +2462,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
QuoteModel quote,
|
||||
List<Contact> contacts,
|
||||
List<LinkPreview> previews,
|
||||
List<Mention> mentions,
|
||||
final long expiresIn,
|
||||
final boolean viewOnce,
|
||||
final int subscriptionId,
|
||||
|
@ -2424,7 +2483,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
}
|
||||
}
|
||||
|
||||
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient.get(), slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, viewOnce, distributionType, quote, contacts, previews);
|
||||
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient.get(), slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, viewOnce, distributionType, quote, contacts, previews, mentions);
|
||||
|
||||
final SettableFuture<Void> future = new SettableFuture<>();
|
||||
final Context context = getApplicationContext();
|
||||
|
@ -2543,7 +2602,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
private void updateLinkPreviewState() {
|
||||
if (TextSecurePreferences.isLinkPreviewsEnabled(this) && !sendButton.getSelectedTransport().isSms() && !attachmentManager.isAttachmentPresent()) {
|
||||
linkPreviewViewModel.onEnabled();
|
||||
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), composeText.getSelectionStart(), composeText.getSelectionEnd());
|
||||
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed().toString(), composeText.getSelectionStart(), composeText.getSelectionEnd());
|
||||
} else {
|
||||
linkPreviewViewModel.onUserCancel();
|
||||
}
|
||||
|
@ -2611,7 +2670,20 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
SlideDeck slideDeck = new SlideDeck();
|
||||
slideDeck.addSlide(audioSlide);
|
||||
|
||||
sendMediaMessage(forceSms, "", slideDeck, inputPanel.getQuote().orNull(), Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, true).addListener(new AssertedSuccessListener<Void>() {
|
||||
ListenableFuture<Void> sendResult = sendMediaMessage(forceSms,
|
||||
"",
|
||||
slideDeck,
|
||||
inputPanel.getQuote().orNull(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
composeText.getMentions(),
|
||||
expiresIn,
|
||||
false,
|
||||
subscriptionId,
|
||||
initiating,
|
||||
true);
|
||||
|
||||
sendResult.addListener(new AssertedSuccessListener<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void nothing) {
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
|
@ -2700,7 +2772,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
|
||||
@Override
|
||||
public void onCursorPositionChanged(int start, int end) {
|
||||
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed(), start, end);
|
||||
linkPreviewViewModel.onTextChanged(this, composeText.getTextTrimmed().toString(), start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -2740,7 +2812,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
|
||||
slideDeck.addSlide(stickerSlide);
|
||||
|
||||
sendMediaMessage(transport.isSms(), "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, clearCompose);
|
||||
sendMediaMessage(transport.isSms(), "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), expiresIn, false, subscriptionId, initiating, clearCompose);
|
||||
}
|
||||
|
||||
private void silentlySetComposeText(String text) {
|
||||
|
@ -2969,7 +3041,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public void handleReplyMessage(MessageRecord messageRecord) {
|
||||
public void handleReplyMessage(ConversationMessage conversationMessage) {
|
||||
MessageRecord messageRecord = conversationMessage.getMessageRecord();
|
||||
|
||||
Recipient author;
|
||||
|
||||
if (messageRecord.isOutgoing()) {
|
||||
|
@ -3005,7 +3079,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
inputPanel.setQuote(GlideApp.with(this),
|
||||
messageRecord.getDateSent(),
|
||||
author,
|
||||
messageRecord.getBody(),
|
||||
conversationMessage.getDisplayBody(this),
|
||||
slideDeck);
|
||||
} else {
|
||||
SlideDeck slideDeck = messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck();
|
||||
|
@ -3019,7 +3093,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
inputPanel.setQuote(GlideApp.with(this),
|
||||
messageRecord.getDateSent(),
|
||||
author,
|
||||
messageRecord.getBody(),
|
||||
conversationMessage.getDisplayBody(this),
|
||||
slideDeck);
|
||||
}
|
||||
|
||||
|
@ -3186,6 +3260,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
quote,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
composeText.getMentions(),
|
||||
expiresIn,
|
||||
false,
|
||||
subscriptionId,
|
||||
|
@ -3244,7 +3319,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
}
|
||||
}
|
||||
|
||||
private class QuoteRestorationTask extends AsyncTask<Void, Void, MessageRecord> {
|
||||
private class QuoteRestorationTask extends AsyncTask<Void, Void, ConversationMessage> {
|
||||
|
||||
private final String serialized;
|
||||
private final SettableFuture<Boolean> future;
|
||||
|
@ -3255,20 +3330,27 @@ public class ConversationActivity extends PassphraseRequiredActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
protected MessageRecord doInBackground(Void... voids) {
|
||||
protected ConversationMessage doInBackground(Void... voids) {
|
||||
QuoteId quoteId = QuoteId.deserialize(ConversationActivity.this, serialized);
|
||||
|
||||
if (quoteId != null) {
|
||||
return DatabaseFactory.getMmsSmsDatabase(getApplicationContext()).getMessageFor(quoteId.getId(), quoteId.getAuthor());
|
||||
if (quoteId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
Context context = getApplicationContext();
|
||||
|
||||
MessageRecord messageRecord = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quoteId.getId(), quoteId.getAuthor());
|
||||
if (messageRecord == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ConversationMessageFactory.createWithUnresolvedData(context, messageRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(MessageRecord messageRecord) {
|
||||
if (messageRecord != null) {
|
||||
handleReplyMessage(messageRecord);
|
||||
protected void onPostExecute(ConversationMessage conversationMessage) {
|
||||
if (conversationMessage != null) {
|
||||
handleReplyMessage(conversationMessage);
|
||||
future.set(true);
|
||||
} else {
|
||||
Log.e(TAG, "Failed to restore a quote from a draft. No matching message record.");
|
||||
|
|
|
@ -4,14 +4,17 @@ import android.content.Context;
|
|||
import android.database.ContentObserver;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.paging.DataSource;
|
||||
import androidx.paging.PositionalDataSource;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
@ -19,7 +22,11 @@ import org.thoughtcrime.securesms.util.paging.Invalidator;
|
|||
import org.thoughtcrime.securesms.util.paging.SizeFixResult;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
|
@ -66,19 +73,24 @@ class ConversationDataSource extends PositionalDataSource<ConversationMessage> {
|
|||
int totalCount = db.getConversationCount(threadId);
|
||||
int effectiveCount = params.requestedStartPosition;
|
||||
|
||||
MentionHelper mentionHelper = new MentionHelper();
|
||||
|
||||
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, params.requestedStartPosition, params.requestedLoadSize))) {
|
||||
MessageRecord record;
|
||||
while ((record = reader.getNext()) != null && effectiveCount < totalCount && !isInvalid()) {
|
||||
records.add(record);
|
||||
mentionHelper.add(record);
|
||||
effectiveCount++;
|
||||
}
|
||||
}
|
||||
|
||||
mentionHelper.fetchMentions(context);
|
||||
|
||||
if (!isInvalid()) {
|
||||
SizeFixResult<MessageRecord> result = SizeFixResult.ensureMultipleOfPageSize(records, params.requestedStartPosition, params.pageSize, totalCount);
|
||||
|
||||
List<ConversationMessage> items = Stream.of(result.getItems())
|
||||
.map(ConversationMessage::new)
|
||||
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId())))
|
||||
.toList();
|
||||
|
||||
callback.onResult(items, params.requestedStartPosition, result.getTotal());
|
||||
|
@ -92,24 +104,48 @@ class ConversationDataSource extends PositionalDataSource<ConversationMessage> {
|
|||
public void loadRange(@NonNull LoadRangeParams params, @NonNull LoadRangeCallback<ConversationMessage> callback) {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
List<MessageRecord> records = new ArrayList<>(params.loadSize);
|
||||
MmsSmsDatabase db = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
List<MessageRecord> records = new ArrayList<>(params.loadSize);
|
||||
MentionHelper mentionHelper = new MentionHelper();
|
||||
|
||||
try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, params.startPosition, params.loadSize))) {
|
||||
MessageRecord record;
|
||||
while ((record = reader.getNext()) != null && !isInvalid()) {
|
||||
records.add(record);
|
||||
mentionHelper.add(record);
|
||||
}
|
||||
}
|
||||
|
||||
mentionHelper.fetchMentions(context);
|
||||
|
||||
List<ConversationMessage> items = Stream.of(records)
|
||||
.map(ConversationMessage::new)
|
||||
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId())))
|
||||
.toList();
|
||||
callback.onResult(items);
|
||||
|
||||
Log.d(TAG, "[Update] " + (System.currentTimeMillis() - start) + " ms | thread: " + threadId + ", start: " + params.startPosition + ", size: " + params.loadSize + (isInvalid() ? " -- invalidated" : ""));
|
||||
}
|
||||
|
||||
private static class MentionHelper {
|
||||
|
||||
private Collection<Long> messageIds = new LinkedList<>();
|
||||
private Map<Long, List<Mention>> messageIdToMentions = new HashMap<>();
|
||||
|
||||
void add(MessageRecord record) {
|
||||
if (record.isMms()) {
|
||||
messageIds.add(record.getId());
|
||||
}
|
||||
}
|
||||
|
||||
void fetchMentions(Context context) {
|
||||
messageIdToMentions = DatabaseFactory.getMentionDatabase(context).getMentionsForMessages(messageIds);
|
||||
}
|
||||
|
||||
@Nullable List<Mention> getMentions(long id) {
|
||||
return messageIdToMentions.get(id);
|
||||
}
|
||||
}
|
||||
|
||||
static class Factory extends DataSource.Factory<Integer, ConversationMessage> {
|
||||
|
||||
private final Context context;
|
||||
|
|
|
@ -18,6 +18,8 @@ package org.thoughtcrime.securesms.conversation;
|
|||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
|
@ -25,7 +27,7 @@ import android.net.Uri;
|
|||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.ClipboardManager;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
|
@ -72,6 +74,7 @@ import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
|||
import org.thoughtcrime.securesms.contactshare.SharedContactDetailsActivity;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter.StickyHeaderViewHolder;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
|
@ -128,7 +131,6 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
@ -595,33 +597,25 @@ public class ConversationFragment extends LoggingFragment {
|
|||
}
|
||||
|
||||
private void handleCopyMessage(final Set<ConversationMessage> conversationMessages) {
|
||||
List<MessageRecord> messageList = Stream.of(conversationMessages).map(ConversationMessage::getMessageRecord).toList();
|
||||
Collections.sort(messageList, new Comparator<MessageRecord>() {
|
||||
@Override
|
||||
public int compare(MessageRecord lhs, MessageRecord rhs) {
|
||||
if (lhs.getDateReceived() < rhs.getDateReceived()) return -1;
|
||||
else if (lhs.getDateReceived() == rhs.getDateReceived()) return 0;
|
||||
else return 1;
|
||||
}
|
||||
});
|
||||
List<ConversationMessage> messageList = new ArrayList<>(conversationMessages);
|
||||
Collections.sort(messageList, (lhs, rhs) -> Long.compare(lhs.getMessageRecord().getDateReceived(), rhs.getMessageRecord().getDateReceived()));
|
||||
|
||||
StringBuilder bodyBuilder = new StringBuilder();
|
||||
ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
SpannableStringBuilder bodyBuilder = new SpannableStringBuilder();
|
||||
ClipboardManager clipboard = (ClipboardManager) requireActivity().getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
|
||||
for (MessageRecord messageRecord : messageList) {
|
||||
String body = messageRecord.getDisplayBody(requireContext()).toString();
|
||||
for (ConversationMessage message : messageList) {
|
||||
CharSequence body = message.getDisplayBody(requireContext());
|
||||
if (!TextUtils.isEmpty(body)) {
|
||||
bodyBuilder.append(body).append('\n');
|
||||
if (bodyBuilder.length() > 0) {
|
||||
bodyBuilder.append('\n');
|
||||
}
|
||||
bodyBuilder.append(body);
|
||||
}
|
||||
}
|
||||
if (bodyBuilder.length() > 0 && bodyBuilder.charAt(bodyBuilder.length() - 1) == '\n') {
|
||||
bodyBuilder.deleteCharAt(bodyBuilder.length() - 1);
|
||||
|
||||
if (!TextUtils.isEmpty(bodyBuilder)) {
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(null, bodyBuilder));
|
||||
}
|
||||
|
||||
String result = bodyBuilder.toString();
|
||||
|
||||
if (!TextUtils.isEmpty(result))
|
||||
clipboard.setText(result);
|
||||
}
|
||||
|
||||
private void handleDeleteMessages(final Set<ConversationMessage> conversationMessages) {
|
||||
|
@ -746,8 +740,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
}
|
||||
|
||||
private void handleForwardMessage(ConversationMessage conversationMessage) {
|
||||
MessageRecord message = conversationMessage.getMessageRecord();
|
||||
if (message.isViewOnce()) {
|
||||
if (conversationMessage.getMessageRecord().isViewOnce()) {
|
||||
throw new AssertionError("Cannot forward a view-once message.");
|
||||
}
|
||||
|
||||
|
@ -755,10 +748,10 @@ public class ConversationFragment extends LoggingFragment {
|
|||
|
||||
SimpleTask.run(getLifecycle(), () -> {
|
||||
Intent composeIntent = new Intent(getActivity(), ShareActivity.class);
|
||||
composeIntent.putExtra(Intent.EXTRA_TEXT, message.getDisplayBody(requireContext()).toString());
|
||||
composeIntent.putExtra(Intent.EXTRA_TEXT, conversationMessage.getDisplayBody(requireContext()));
|
||||
|
||||
if (message.isMms()) {
|
||||
MmsMessageRecord mediaMessage = (MmsMessageRecord) message;
|
||||
if (conversationMessage.getMessageRecord().isMms()) {
|
||||
MmsMessageRecord mediaMessage = (MmsMessageRecord) conversationMessage.getMessageRecord();
|
||||
boolean isAlbum = mediaMessage.containsMediaSlide() &&
|
||||
mediaMessage.getSlideDeck().getSlides().size() > 1 &&
|
||||
mediaMessage.getSlideDeck().getAudioSlide() == null &&
|
||||
|
@ -788,7 +781,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
Optional.fromNullable(attachment.getCaption()),
|
||||
Optional.absent()));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!mediaList.isEmpty()) {
|
||||
composeIntent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaList);
|
||||
|
@ -835,7 +828,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
((AppCompatActivity) getActivity()).getSupportActionBar().collapseActionView();
|
||||
}
|
||||
|
||||
listener.handleReplyMessage(message.getMessageRecord());
|
||||
listener.handleReplyMessage(message);
|
||||
}
|
||||
|
||||
private void handleSaveAttachment(final MediaMmsMessageRecord message) {
|
||||
|
@ -875,7 +868,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
if (getListAdapter() != null) {
|
||||
clearHeaderIfNotTyping(getListAdapter());
|
||||
setLastSeen(0);
|
||||
getListAdapter().addFastRecord(new ConversationMessage(messageRecord));
|
||||
getListAdapter().addFastRecord(ConversationMessageFactory.createWithResolvedData(messageRecord, messageRecord.getDisplayBody(requireContext()), message.getMentions()));
|
||||
list.post(() -> list.scrollToPosition(0));
|
||||
}
|
||||
|
||||
|
@ -888,7 +881,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
if (getListAdapter() != null) {
|
||||
clearHeaderIfNotTyping(getListAdapter());
|
||||
setLastSeen(0);
|
||||
getListAdapter().addFastRecord(new ConversationMessage(messageRecord));
|
||||
getListAdapter().addFastRecord(ConversationMessageFactory.createWithResolvedData(messageRecord));
|
||||
list.post(() -> list.scrollToPosition(0));
|
||||
}
|
||||
|
||||
|
@ -1017,7 +1010,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
|
||||
public interface ConversationFragmentListener {
|
||||
void setThreadId(long threadId);
|
||||
void handleReplyMessage(MessageRecord messageRecord);
|
||||
void handleReplyMessage(ConversationMessage conversationMessage);
|
||||
void onMessageActionToolbarOpened();
|
||||
void onForwardClicked();
|
||||
void onMessageRequest(@NonNull MessageRequestViewModel viewModel);
|
||||
|
@ -1306,7 +1299,7 @@ public class ConversationFragment extends LoggingFragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onGroupMemberAvatarClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId) {
|
||||
public void onGroupMemberClicked(@NonNull RecipientId recipientId, @NonNull GroupId groupId) {
|
||||
if (getContext() == null) return;
|
||||
|
||||
RecipientBottomSheetDialogFragment.create(recipientId, groupId).show(requireFragmentManager(), "BOTTOM");
|
||||
|
|
|
@ -26,6 +26,7 @@ import android.graphics.PorterDuff;
|
|||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.text.Annotation;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
@ -71,6 +72,7 @@ import org.thoughtcrime.securesms.components.QuoteView;
|
|||
import org.thoughtcrime.securesms.components.SharedContactView;
|
||||
import org.thoughtcrime.securesms.components.BorderlessImageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
|
@ -552,7 +554,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||
} else if (isCaptionlessMms(messageRecord)) {
|
||||
bodyText.setVisibility(View.GONE);
|
||||
} else {
|
||||
Spannable styledText = linkifyMessageBody(messageRecord.getDisplayBody(getContext()), batchSelected.isEmpty());
|
||||
Spannable styledText = linkifyMessageBody(conversationMessage.getDisplayBody(getContext()), batchSelected.isEmpty());
|
||||
styledText = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), styledText, searchQuery);
|
||||
styledText = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), styledText, searchQuery);
|
||||
|
||||
|
@ -855,7 +857,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||
|
||||
contactPhoto.setOnClickListener(v -> {
|
||||
if (eventListener != null) {
|
||||
eventListener.onGroupMemberAvatarClicked(recipientId, conversationRecipient.get().requireGroupId());
|
||||
eventListener.onGroupMemberClicked(recipientId, conversationRecipient.get().requireGroupId());
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -879,6 +881,12 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||
messageBody.setSpan(new LongClickCopySpan(urlSpan.getURL()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
List<Annotation> mentionAnnotations = MentionAnnotation.getMentionAnnotations(messageBody);
|
||||
for (Annotation annotation : mentionAnnotations) {
|
||||
messageBody.setSpan(new MentionClickableSpan(RecipientId.from(annotation.getValue())), messageBody.getSpanStart(annotation), messageBody.getSpanEnd(annotation), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
return messageBody;
|
||||
}
|
||||
|
||||
|
@ -901,7 +909,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||
}
|
||||
Quote quote = ((MediaMmsMessageRecord)current).getQuote();
|
||||
//noinspection ConstantConditions
|
||||
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getText(), quote.isOriginalMissing(), quote.getAttachment());
|
||||
quoteView.setQuote(glideRequests, quote.getId(), Recipient.live(quote.getAuthor()).get(), quote.getDisplayText(), quote.isOriginalMissing(), quote.getAttachment());
|
||||
quoteView.setVisibility(View.VISIBLE);
|
||||
quoteView.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
|
||||
|
@ -1405,6 +1413,24 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||
}
|
||||
}
|
||||
|
||||
private class MentionClickableSpan extends ClickableSpan {
|
||||
private final RecipientId mentionedRecipientId;
|
||||
|
||||
MentionClickableSpan(RecipientId mentionedRecipientId) {
|
||||
this.mentionedRecipientId = mentionedRecipientId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
if (eventListener != null && !Recipient.resolved(mentionedRecipientId).isLocalNumber()) {
|
||||
eventListener.onGroupMemberClicked(mentionedRecipientId, conversationRecipient.get().requireGroupId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(@NonNull TextPaint ds) { }
|
||||
}
|
||||
|
||||
private void handleMessageApproval() {
|
||||
final int title;
|
||||
final int message;
|
||||
|
|
|
@ -1,27 +1,58 @@
|
|||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.content.Context;
|
||||
import android.text.SpannableString;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A view level model used to pass arbitrary message related information needed
|
||||
* for various presentations.
|
||||
*/
|
||||
public class ConversationMessage {
|
||||
private final MessageRecord messageRecord;
|
||||
@NonNull private final MessageRecord messageRecord;
|
||||
@NonNull private final List<Mention> mentions;
|
||||
@Nullable private final SpannableString body;
|
||||
|
||||
public ConversationMessage(@NonNull MessageRecord messageRecord) {
|
||||
private ConversationMessage(@NonNull MessageRecord messageRecord) {
|
||||
this(messageRecord, null, null);
|
||||
}
|
||||
|
||||
private ConversationMessage(@NonNull MessageRecord messageRecord,
|
||||
@Nullable CharSequence body,
|
||||
@Nullable List<Mention> mentions)
|
||||
{
|
||||
this.messageRecord = messageRecord;
|
||||
this.body = body != null ? SpannableString.valueOf(body) : null;
|
||||
this.mentions = mentions != null ? mentions : Collections.emptyList();
|
||||
|
||||
if (!this.mentions.isEmpty() && this.body != null) {
|
||||
MentionAnnotation.setMentionAnnotations(this.body, this.mentions);
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull MessageRecord getMessageRecord() {
|
||||
return messageRecord;
|
||||
}
|
||||
|
||||
public @NonNull List<Mention> getMentions() {
|
||||
return mentions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
@ -41,4 +72,74 @@ public class ConversationMessage {
|
|||
|
||||
return Conversions.byteArrayToLong(bytes);
|
||||
}
|
||||
|
||||
public @NonNull SpannableString getDisplayBody(Context context) {
|
||||
if (mentions.isEmpty() || body == null) {
|
||||
return messageRecord.getDisplayBody(context);
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory providing multiple ways of creating {@link ConversationMessage}s.
|
||||
*/
|
||||
public static class ConversationMessageFactory {
|
||||
|
||||
/**
|
||||
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord. No database or
|
||||
* heavy work performed as the message is assumed to not have any mentions.
|
||||
*/
|
||||
@AnyThread
|
||||
public static @NonNull ConversationMessage createWithResolvedData(@NonNull MessageRecord messageRecord) {
|
||||
return new ConversationMessage(messageRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord, potentially annotated body, and
|
||||
* list of actual mentions. No database or heavy work performed as the body and mentions are assumed to be
|
||||
* fully updated with display names.
|
||||
*
|
||||
* @param body Contains appropriate {@link MentionAnnotation}s and is updated with actual profile names.
|
||||
* @param mentions List of actual mentions (i.e., not placeholder) matching annotation ranges in body.
|
||||
*/
|
||||
@AnyThread
|
||||
public static @NonNull ConversationMessage createWithResolvedData(@NonNull MessageRecord messageRecord, @Nullable CharSequence body, @Nullable List<Mention> mentions) {
|
||||
if (messageRecord.isMms() && mentions != null && !mentions.isEmpty()) {
|
||||
return new ConversationMessage(messageRecord, body, mentions);
|
||||
}
|
||||
return createWithResolvedData(messageRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord and will update and modify the provided
|
||||
* mentions from placeholder to actual. This method may perform database operations to resolve mentions to display names.
|
||||
*
|
||||
* @param mentions List of placeholder mentions to be used to update the body in the provided MessageRecord.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, @Nullable List<Mention> mentions) {
|
||||
if (messageRecord.isMms() && mentions != null && !mentions.isEmpty()) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, messageRecord, mentions);
|
||||
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions());
|
||||
}
|
||||
return createWithResolvedData(messageRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord, and will query for potential mentions. If mentions
|
||||
* are found, the body of the provided message will be updated and modified to match actual mentions. This will perform
|
||||
* database operations to query for mentions and then to resolve mentions to display names.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord) {
|
||||
if (messageRecord.isMms()) {
|
||||
List<Mention> mentions = DatabaseFactory.getMentionDatabase(context).getMentionsForMessage(messageRecord.getId());
|
||||
if (!mentions.isEmpty()) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, messageRecord, mentions);
|
||||
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions());
|
||||
}
|
||||
}
|
||||
return createWithResolvedData(messageRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,20 +16,24 @@ public class MentionViewHolder extends MappingViewHolder<MentionViewState> {
|
|||
|
||||
private final AvatarImageView avatar;
|
||||
private final TextView name;
|
||||
private final TextView username;
|
||||
|
||||
@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);
|
||||
avatar = findViewById(R.id.mention_recipient_avatar);
|
||||
name = findViewById(R.id.mention_recipient_name);
|
||||
username = findViewById(R.id.mention_recipient_username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull MentionViewState model) {
|
||||
avatar.setRecipient(model.getRecipient());
|
||||
name.setText(model.getName(context));
|
||||
username.setText(model.getUsername());
|
||||
itemView.setOnClickListener(v -> {
|
||||
if (mentionEventsListener != null) {
|
||||
mentionEventsListener.onMentionClicked(model.getRecipient());
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
|
|||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MappingModel;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
|
@ -26,6 +27,10 @@ public final class MentionViewState implements MappingModel<MentionViewState> {
|
|||
return recipient;
|
||||
}
|
||||
|
||||
@NonNull String getUsername() {
|
||||
return Util.emptyIfNull(recipient.getDisplayUsername());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull MentionViewState newItem) {
|
||||
return recipient.getId().equals(newItem.recipient.getId());
|
||||
|
|
|
@ -27,13 +27,15 @@ public class MentionsPickerFragment extends LoggingFragment {
|
|||
private RecyclerView list;
|
||||
private BottomSheetBehavior<View> behavior;
|
||||
private MentionsPickerViewModel viewModel;
|
||||
private int defaultPeekHeight;
|
||||
|
||||
@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));
|
||||
list = view.findViewById(R.id.mentions_picker_list);
|
||||
behavior = BottomSheetBehavior.from(view.findViewById(R.id.mentions_picker_bottom_sheet));
|
||||
defaultPeekHeight = view.getContext().getResources().getDimensionPixelSize(R.dimen.mentions_picker_peek_height);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
@ -72,13 +74,16 @@ public class MentionsPickerFragment extends LoggingFragment {
|
|||
if (mappingModels.isEmpty()) {
|
||||
updateBottomSheetBehavior(0);
|
||||
}
|
||||
list.scrollToPosition(0);
|
||||
}
|
||||
|
||||
private void updateBottomSheetBehavior(int count) {
|
||||
if (count > 0) {
|
||||
if (behavior.getPeekHeight() == 0) {
|
||||
behavior.setPeekHeight(ViewUtil.dpToPx(240), true);
|
||||
behavior.setPeekHeight(defaultPeekHeight, true);
|
||||
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
} else {
|
||||
list.scrollToPosition(0);
|
||||
}
|
||||
} else {
|
||||
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
final class MentionsPickerRepository {
|
||||
|
||||
private final RecipientDatabase recipientDatabase;
|
||||
|
||||
MentionsPickerRepository(@NonNull Context context) {
|
||||
recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@NonNull List<Recipient> search(MentionQuery mentionQuery) {
|
||||
if (TextUtils.isEmpty(mentionQuery.query)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<RecipientId> recipientIds = Stream.of(mentionQuery.members)
|
||||
.filterNot(m -> m.getMember().isLocalNumber())
|
||||
.map(m -> m.getMember().getId())
|
||||
.toList();
|
||||
|
||||
return recipientDatabase.queryRecipientsForMentions(mentionQuery.query, recipientIds);
|
||||
}
|
||||
|
||||
static class MentionQuery {
|
||||
private final String query;
|
||||
private final List<GroupMemberEntry.FullMember> members;
|
||||
|
||||
MentionQuery(@NonNull String query, @NonNull List<GroupMemberEntry.FullMember> members) {
|
||||
this.query = query;
|
||||
this.members = members;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,5 @@
|
|||
package org.thoughtcrime.securesms.conversation.ui.mentions;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
@ -11,6 +9,7 @@ import androidx.lifecycle.ViewModelProvider;
|
|||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerRepository.MentionQuery;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.groups.LiveGroup;
|
||||
|
@ -20,7 +19,6 @@ 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 {
|
||||
|
@ -28,17 +26,18 @@ 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;
|
||||
private final MutableLiveData<String> liveQuery;
|
||||
|
||||
MentionsPickerViewModel() {
|
||||
MentionsPickerViewModel(@NonNull MentionsPickerRepository mentionsPickerRepository) {
|
||||
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));
|
||||
LiveData<List<FullMember>> fullMembers = Transformations.distinctUntilChanged(Transformations.switchMap(group, LiveGroup::getFullMembers));
|
||||
LiveData<String> query = Transformations.distinctUntilChanged(liveQuery);
|
||||
LiveData<MentionQuery> mentionQuery = LiveDataUtil.combineLatest(query, fullMembers, MentionQuery::new);
|
||||
|
||||
mentionList = LiveDataUtil.combineLatest(Transformations.distinctUntilChanged(liveQuery), members, this::filterMembers);
|
||||
mentionList = LiveDataUtil.mapAsync(mentionQuery, q -> Stream.of(mentionsPickerRepository.search(q)).<MappingModel<?>>map(MentionViewState::new).toList());
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<MappingModel<?>>> getMentionList() {
|
||||
|
@ -54,7 +53,7 @@ public class MentionsPickerViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
public void onQueryChange(@NonNull CharSequence query) {
|
||||
liveQuery.setValue(query);
|
||||
liveQuery.setValue(query.toString());
|
||||
}
|
||||
|
||||
public void onRecipientChange(@NonNull Recipient recipient) {
|
||||
|
@ -65,22 +64,11 @@ public class MentionsPickerViewModel extends ViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
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());
|
||||
return modelClass.cast(new MentionsPickerViewModel(new MentionsPickerRepository(ApplicationDependencies.getApplication())));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -63,6 +63,7 @@ public class DatabaseFactory {
|
|||
private final KeyValueDatabase keyValueDatabase;
|
||||
private final MegaphoneDatabase megaphoneDatabase;
|
||||
private final RemappedRecordsDatabase remappedRecordsDatabase;
|
||||
private final MentionDatabase mentionDatabase;
|
||||
|
||||
public static DatabaseFactory getInstance(Context context) {
|
||||
synchronized (lock) {
|
||||
|
@ -165,6 +166,10 @@ public class DatabaseFactory {
|
|||
return getInstance(context).remappedRecordsDatabase;
|
||||
}
|
||||
|
||||
public static MentionDatabase getMentionDatabase(Context context) {
|
||||
return getInstance(context).mentionDatabase;
|
||||
}
|
||||
|
||||
public static SQLiteDatabase getBackupDatabase(Context context) {
|
||||
return getInstance(context).databaseHelper.getReadableDatabase();
|
||||
}
|
||||
|
@ -214,6 +219,7 @@ public class DatabaseFactory {
|
|||
this.keyValueDatabase = new KeyValueDatabase(context, databaseHelper);
|
||||
this.megaphoneDatabase = new MegaphoneDatabase(context, databaseHelper);
|
||||
this.remappedRecordsDatabase = new RemappedRecordsDatabase(context, databaseHelper);
|
||||
this.mentionDatabase = new MentionDatabase(context, databaseHelper);
|
||||
}
|
||||
|
||||
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,
|
||||
|
|
|
@ -4,6 +4,8 @@ import android.content.ContentValues;
|
|||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
@ -73,14 +75,11 @@ public class DraftDatabase extends Database {
|
|||
db.delete(TABLE_NAME, null, null);
|
||||
}
|
||||
|
||||
public List<Draft> getDrafts(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
List<Draft> results = new LinkedList<>();
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = db.query(TABLE_NAME, null, THREAD_ID + " = ?", new String[] {threadId+""}, null, null, null);
|
||||
public Drafts getDrafts(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Drafts results = new Drafts();
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, null, THREAD_ID + " = ?", new String[] {threadId+""}, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String type = cursor.getString(cursor.getColumnIndexOrThrow(DRAFT_TYPE));
|
||||
String value = cursor.getString(cursor.getColumnIndexOrThrow(DRAFT_VALUE));
|
||||
|
@ -89,9 +88,6 @@ public class DraftDatabase extends Database {
|
|||
}
|
||||
|
||||
return results;
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,6 +98,7 @@ public class DraftDatabase extends Database {
|
|||
public static final String AUDIO = "audio";
|
||||
public static final String LOCATION = "location";
|
||||
public static final String QUOTE = "quote";
|
||||
public static final String MENTION = "mention";
|
||||
|
||||
private final String type;
|
||||
private final String value;
|
||||
|
@ -133,7 +130,7 @@ public class DraftDatabase extends Database {
|
|||
}
|
||||
|
||||
public static class Drafts extends LinkedList<Draft> {
|
||||
private Draft getDraftOfType(String type) {
|
||||
public @Nullable Draft getDraftOfType(String type) {
|
||||
for (Draft draft : this) {
|
||||
if (type.equals(draft.getType())) {
|
||||
return draft;
|
||||
|
@ -142,7 +139,7 @@ public class DraftDatabase extends Database {
|
|||
return null;
|
||||
}
|
||||
|
||||
public String getSnippet(Context context) {
|
||||
public @NonNull String getSnippet(Context context) {
|
||||
Draft textDraft = getDraftOfType(Draft.TEXT);
|
||||
if (textDraft != null) {
|
||||
return textDraft.getSnippet(context);
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.SqlUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class MentionDatabase extends Database {
|
||||
|
||||
private static final String TABLE_NAME = "mention";
|
||||
|
||||
private static final String ID = "_id";
|
||||
private static final String THREAD_ID = "thread_id";
|
||||
private static final String MESSAGE_ID = "message_id";
|
||||
private static final String RECIPIENT_ID = "recipient_id";
|
||||
private static final String RANGE_START = "range_start";
|
||||
private static final String RANGE_LENGTH = "range_length";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
THREAD_ID + " INTEGER, " +
|
||||
MESSAGE_ID + " INTEGER, " +
|
||||
RECIPIENT_ID + " INTEGER, " +
|
||||
RANGE_START + " INTEGER, " +
|
||||
RANGE_LENGTH + " INTEGER)";
|
||||
|
||||
public static final String[] CREATE_INDEXES = new String[] {
|
||||
"CREATE INDEX IF NOT EXISTS mention_message_id_index ON " + TABLE_NAME + " (" + MESSAGE_ID + ");",
|
||||
"CREATE INDEX IF NOT EXISTS mention_recipient_id_thread_id_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ", " + THREAD_ID + ");"
|
||||
};
|
||||
|
||||
public MentionDatabase(@NonNull Context context, @NonNull SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public void insert(long threadId, long messageId, @NonNull Collection<Mention> mentions) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
for (Mention mention : mentions) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(THREAD_ID, threadId);
|
||||
values.put(MESSAGE_ID, messageId);
|
||||
values.put(RECIPIENT_ID, mention.getRecipientId().toLong());
|
||||
values.put(RANGE_START, mention.getStart());
|
||||
values.put(RANGE_LENGTH, mention.getLength());
|
||||
db.insert(TABLE_NAME, null, values);
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull List<Mention> getMentionsForMessage(long messageId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
List<Mention> mentions = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, null, MESSAGE_ID + " = ?", SqlUtil.buildArgs(messageId), null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
mentions.add(new Mention(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)),
|
||||
CursorUtil.requireInt(cursor, RANGE_START),
|
||||
CursorUtil.requireInt(cursor, RANGE_LENGTH)));
|
||||
}
|
||||
}
|
||||
|
||||
return mentions;
|
||||
}
|
||||
|
||||
public @NonNull Map<Long, List<Mention>> getMentionsForMessages(@NonNull Collection<Long> messageIds) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Map<Long, List<Mention>> mentions = new HashMap<>();
|
||||
|
||||
String ids = TextUtils.join(",", messageIds);
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, null, MESSAGE_ID + " IN (" + ids + ")", null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
long messageId = CursorUtil.requireLong(cursor, MESSAGE_ID);
|
||||
List<Mention> messageMentions = mentions.get(messageId);
|
||||
|
||||
if (messageMentions == null) {
|
||||
messageMentions = new LinkedList<>();
|
||||
mentions.put(messageId, messageMentions);
|
||||
}
|
||||
|
||||
messageMentions.add(new Mention(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)),
|
||||
CursorUtil.requireInt(cursor, RANGE_START),
|
||||
CursorUtil.requireInt(cursor, RANGE_LENGTH)));
|
||||
}
|
||||
}
|
||||
|
||||
return mentions;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.SpannableStringBuilder;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.annimon.stream.function.Function;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.MentionSetting;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class MentionUtil {
|
||||
|
||||
public static final char MENTION_STARTER = '@';
|
||||
static final String MENTION_PLACEHOLDER = "\uFFFC";
|
||||
|
||||
private MentionUtil() { }
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull CharSequence updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord) {
|
||||
return updateBodyWithDisplayNames(context, messageRecord, messageRecord.getDisplayBody(context));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull CharSequence updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body) {
|
||||
if (messageRecord.isMms()) {
|
||||
List<Mention> mentions = DatabaseFactory.getMentionDatabase(context).getMentionsForMessage(messageRecord.getId());
|
||||
CharSequence updated = updateBodyAndMentionsWithDisplayNames(context, body, mentions).getBody();
|
||||
if (updated != null) {
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull UpdatedBodyAndMentions updateBodyAndMentionsWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull List<Mention> mentions) {
|
||||
return updateBodyAndMentionsWithDisplayNames(context, messageRecord.getDisplayBody(context), mentions);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull UpdatedBodyAndMentions updateBodyAndMentionsWithDisplayNames(@NonNull Context context, @NonNull CharSequence body, @NonNull List<Mention> mentions) {
|
||||
return update(body, mentions, m -> MENTION_STARTER + Recipient.resolved(m.getRecipientId()).getDisplayName(context));
|
||||
}
|
||||
|
||||
public static @NonNull UpdatedBodyAndMentions updateBodyAndMentionsWithPlaceholders(@Nullable CharSequence body, @NonNull List<Mention> mentions) {
|
||||
return update(body, mentions, m -> MENTION_PLACEHOLDER);
|
||||
}
|
||||
|
||||
private static @NonNull UpdatedBodyAndMentions update(@Nullable CharSequence body, @NonNull List<Mention> mentions, @NonNull Function<Mention, CharSequence> replacementTextGenerator) {
|
||||
if (body == null || mentions.isEmpty()) {
|
||||
return new UpdatedBodyAndMentions(body, mentions);
|
||||
}
|
||||
|
||||
SpannableStringBuilder updatedBody = new SpannableStringBuilder();
|
||||
List<Mention> updatedMentions = new ArrayList<>();
|
||||
|
||||
Collections.sort(mentions);
|
||||
|
||||
int bodyIndex = 0;
|
||||
|
||||
for (Mention mention : mentions) {
|
||||
updatedBody.append(body.subSequence(bodyIndex, mention.getStart()));
|
||||
CharSequence replaceWith = replacementTextGenerator.apply(mention);
|
||||
Mention updatedMention = new Mention(mention.getRecipientId(), updatedBody.length(), replaceWith.length());
|
||||
|
||||
updatedBody.append(replaceWith);
|
||||
updatedMentions.add(updatedMention);
|
||||
|
||||
bodyIndex = mention.getStart() + mention.getLength();
|
||||
}
|
||||
|
||||
if (bodyIndex < body.length()) {
|
||||
updatedBody.append(body.subSequence(bodyIndex, body.length()));
|
||||
}
|
||||
|
||||
return new UpdatedBodyAndMentions(updatedBody.toString(), updatedMentions);
|
||||
}
|
||||
|
||||
public static @Nullable BodyRangeList mentionsToBodyRangeList(@Nullable List<Mention> mentions) {
|
||||
if (mentions == null || mentions.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BodyRangeList.Builder builder = BodyRangeList.newBuilder();
|
||||
|
||||
for (Mention mention : mentions) {
|
||||
String uuid = Recipient.resolved(mention.getRecipientId()).requireUuid().toString();
|
||||
builder.addRanges(BodyRangeList.BodyRange.newBuilder()
|
||||
.setMentionUuid(uuid)
|
||||
.setStart(mention.getStart())
|
||||
.setLength(mention.getLength()));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static @NonNull List<Mention> bodyRangeListToMentions(@NonNull Context context, @Nullable byte[] data) {
|
||||
if (data != null) {
|
||||
try {
|
||||
return Stream.of(BodyRangeList.parseFrom(data).getRangesList())
|
||||
.filter(bodyRange -> bodyRange.getAssociatedValueCase() == BodyRangeList.BodyRange.AssociatedValueCase.MENTIONUUID)
|
||||
.map(mention -> {
|
||||
RecipientId id = Recipient.externalPush(context, UuidUtil.parseOrThrow(mention.getMentionUuid()), null, false).getId();
|
||||
return new Mention(id, mention.getStart(), mention.getLength());
|
||||
})
|
||||
.toList();
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull String getMentionSettingDisplayValue(@NonNull Context context, @NonNull MentionSetting mentionSetting) {
|
||||
switch (mentionSetting) {
|
||||
case GLOBAL:
|
||||
return context.getString(SignalStore.notificationSettings().isMentionNotifiesMeEnabled() ? R.string.GroupMentionSettingDialog_default_notify_me
|
||||
: R.string.GroupMentionSettingDialog_default_dont_notify_me);
|
||||
case ALWAYS_NOTIFY:
|
||||
return context.getString(R.string.GroupMentionSettingDialog_always_notify_me);
|
||||
case DO_NOT_NOTIFY:
|
||||
return context.getString(R.string.GroupMentionSettingDialog_dont_notify_me);
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown mention setting: " + mentionSetting);
|
||||
}
|
||||
|
||||
public static class UpdatedBodyAndMentions {
|
||||
@Nullable private final CharSequence body;
|
||||
@NonNull private final List<Mention> mentions;
|
||||
|
||||
public UpdatedBodyAndMentions(@Nullable CharSequence body, @NonNull List<Mention> mentions) {
|
||||
this.body = body;
|
||||
this.mentions = mentions;
|
||||
}
|
||||
|
||||
public @Nullable CharSequence getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public @NonNull List<Mention> getMentions() {
|
||||
return mentions;
|
||||
}
|
||||
|
||||
@Nullable String getBodyAsString() {
|
||||
return body != null ? body.toString() : null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -47,10 +47,12 @@ import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
|||
import org.thoughtcrime.securesms.database.documents.NetworkFailureList;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.Quote;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
|
@ -68,6 +70,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
|
||||
import org.thoughtcrime.securesms.util.CursorUtil;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
@ -109,9 +112,11 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
static final String QUOTE_BODY = "quote_body";
|
||||
static final String QUOTE_ATTACHMENT = "quote_attachment";
|
||||
static final String QUOTE_MISSING = "quote_missing";
|
||||
static final String QUOTE_MENTIONS = "quote_mentions";
|
||||
|
||||
static final String SHARED_CONTACTS = "shared_contacts";
|
||||
static final String LINK_PREVIEWS = "previews";
|
||||
static final String MENTIONS_SELF = "mentions_self";
|
||||
|
||||
public static final String VIEW_ONCE = "reveal_duration";
|
||||
|
||||
|
@ -163,6 +168,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
QUOTE_BODY + " TEXT, " +
|
||||
QUOTE_ATTACHMENT + " INTEGER DEFAULT -1, " +
|
||||
QUOTE_MISSING + " INTEGER DEFAULT 0, " +
|
||||
QUOTE_MENTIONS + " BLOB DEFAULT NULL," +
|
||||
SHARED_CONTACTS + " TEXT, " +
|
||||
UNIDENTIFIED + " INTEGER DEFAULT 0, " +
|
||||
LINK_PREVIEWS + " TEXT, " +
|
||||
|
@ -170,7 +176,8 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
REACTIONS + " BLOB DEFAULT NULL, " +
|
||||
REACTIONS_UNREAD + " INTEGER DEFAULT 0, " +
|
||||
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " +
|
||||
REMOTE_DELETED + " INTEGER DEFAULT 0);";
|
||||
REMOTE_DELETED + " INTEGER DEFAULT 0, " +
|
||||
MENTIONS_SELF + " INTEGER DEFAULT 0);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
|
||||
|
@ -193,9 +200,9 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
MESSAGE_SIZE, STATUS, TRANSACTION_ID,
|
||||
BODY, PART_COUNT, RECIPIENT_ID, ADDRESS_DEVICE_ID,
|
||||
DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID,
|
||||
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_MISSING,
|
||||
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_MISSING, QUOTE_MENTIONS,
|
||||
SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN,
|
||||
REMOTE_DELETED,
|
||||
REMOTE_DELETED, MENTIONS_SELF,
|
||||
"json_group_array(json_object(" +
|
||||
"'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
|
||||
"'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " +
|
||||
|
@ -805,6 +812,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
throws MmsException, NoSuchMessageException
|
||||
{
|
||||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
MentionDatabase mentionDatabase = DatabaseFactory.getMentionDatabase(context);
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
|
@ -812,6 +820,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
|
||||
if (cursor != null && cursor.moveToNext()) {
|
||||
List<DatabaseAttachment> associatedAttachments = attachmentDatabase.getAttachmentsForMessage(messageId);
|
||||
List<Mention> mentions = mentionDatabase.getMentionsForMessage(messageId);
|
||||
|
||||
long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX));
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(BODY));
|
||||
|
@ -830,6 +839,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
String quoteText = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_BODY));
|
||||
boolean quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE_MISSING)) == 1;
|
||||
List<Attachment> quoteAttachments = Stream.of(associatedAttachments).filter(Attachment::isQuote).map(a -> (Attachment)a).toList();
|
||||
List<Mention> quoteMentions = parseQuoteMentions(cursor);
|
||||
List<Contact> contacts = getSharedContacts(cursor, associatedAttachments);
|
||||
Set<Attachment> contactAttachments = new HashSet<>(Stream.of(contacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList());
|
||||
List<LinkPreview> previews = getLinkPreviews(cursor, associatedAttachments);
|
||||
|
@ -846,7 +856,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
QuoteModel quote = null;
|
||||
|
||||
if (quoteId > 0 && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || !quoteAttachments.isEmpty())) {
|
||||
quote = new QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteAttachments);
|
||||
quote = new QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteAttachments, quoteMentions);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(mismatchDocument)) {
|
||||
|
@ -866,12 +876,12 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
}
|
||||
|
||||
if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) {
|
||||
return new OutgoingGroupUpdateMessage(recipient, new MessageGroupContext(body, Types.isGroupV2(outboxType)), attachments, timestamp, 0, false, quote, contacts, previews);
|
||||
return new OutgoingGroupUpdateMessage(recipient, new MessageGroupContext(body, Types.isGroupV2(outboxType)), attachments, timestamp, 0, false, quote, contacts, previews, mentions);
|
||||
} else if (Types.isExpirationTimerUpdate(outboxType)) {
|
||||
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
|
||||
}
|
||||
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, viewOnce, distributionType, quote, contacts, previews, networkFailures, mismatches);
|
||||
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, viewOnce, distributionType, quote, contacts, previews, mentions, networkFailures, mismatches);
|
||||
|
||||
if (Types.isSecureType(outboxType)) {
|
||||
return new OutgoingSecureMediaMessage(message);
|
||||
|
@ -1000,10 +1010,15 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
|
||||
if (retrieved.getQuote() != null) {
|
||||
contentValues.put(QUOTE_ID, retrieved.getQuote().getId());
|
||||
contentValues.put(QUOTE_BODY, retrieved.getQuote().getText());
|
||||
contentValues.put(QUOTE_BODY, retrieved.getQuote().getText().toString());
|
||||
contentValues.put(QUOTE_AUTHOR, retrieved.getQuote().getAuthor().serialize());
|
||||
contentValues.put(QUOTE_MISSING, retrieved.getQuote().isOriginalMissing() ? 1 : 0);
|
||||
|
||||
BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(retrieved.getQuote().getMentions());
|
||||
if (mentionsList != null) {
|
||||
contentValues.put(QUOTE_MENTIONS, mentionsList.toByteArray());
|
||||
}
|
||||
|
||||
quoteAttachments = retrieved.getQuote().getAttachments();
|
||||
}
|
||||
|
||||
|
@ -1012,7 +1027,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
return Optional.absent();
|
||||
}
|
||||
|
||||
long messageId = insertMediaMessage(retrieved.getBody(), retrieved.getAttachments(), quoteAttachments, retrieved.getSharedContacts(), retrieved.getLinkPreviews(), contentValues, null);
|
||||
long messageId = insertMediaMessage(threadId, retrieved.getBody(), retrieved.getAttachments(), quoteAttachments, retrieved.getSharedContacts(), retrieved.getLinkPreviews(), retrieved.getMentions(), contentValues, null);
|
||||
|
||||
if (!Types.isExpirationTimerUpdate(mailbox)) {
|
||||
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
|
||||
|
@ -1158,15 +1173,23 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
List<Attachment> quoteAttachments = new LinkedList<>();
|
||||
|
||||
if (message.getOutgoingQuote() != null) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithPlaceholders(message.getOutgoingQuote().getText(), message.getOutgoingQuote().getMentions());
|
||||
|
||||
contentValues.put(QUOTE_ID, message.getOutgoingQuote().getId());
|
||||
contentValues.put(QUOTE_AUTHOR, message.getOutgoingQuote().getAuthor().serialize());
|
||||
contentValues.put(QUOTE_BODY, message.getOutgoingQuote().getText());
|
||||
contentValues.put(QUOTE_BODY, updated.getBodyAsString());
|
||||
contentValues.put(QUOTE_MISSING, message.getOutgoingQuote().isOriginalMissing() ? 1 : 0);
|
||||
|
||||
BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(updated.getMentions());
|
||||
if (mentionsList != null) {
|
||||
contentValues.put(QUOTE_MENTIONS, mentionsList.toByteArray());
|
||||
}
|
||||
|
||||
quoteAttachments.addAll(message.getOutgoingQuote().getAttachments());
|
||||
}
|
||||
|
||||
long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), contentValues, insertListener);
|
||||
MentionUtil.UpdatedBodyAndMentions updatedBodyAndMentions = MentionUtil.updateBodyAndMentionsWithPlaceholders(message.getBody(), message.getMentions());
|
||||
long messageId = insertMediaMessage(threadId, updatedBodyAndMentions.getBodyAsString(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), updatedBodyAndMentions.getMentions(), contentValues, insertListener);
|
||||
|
||||
if (message.getRecipient().isGroup()) {
|
||||
OutgoingGroupUpdateMessage outgoingGroupUpdateMessage = (message instanceof OutgoingGroupUpdateMessage) ? (OutgoingGroupUpdateMessage) message : null;
|
||||
|
@ -1197,17 +1220,22 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
return messageId;
|
||||
}
|
||||
|
||||
private long insertMediaMessage(@Nullable String body,
|
||||
private long insertMediaMessage(long threadId,
|
||||
@Nullable String body,
|
||||
@NonNull List<Attachment> attachments,
|
||||
@NonNull List<Attachment> quoteAttachments,
|
||||
@NonNull List<Contact> sharedContacts,
|
||||
@NonNull List<LinkPreview> linkPreviews,
|
||||
@NonNull List<Mention> mentions,
|
||||
@NonNull ContentValues contentValues,
|
||||
@Nullable SmsDatabase.InsertListener insertListener)
|
||||
throws MmsException
|
||||
{
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
AttachmentDatabase partsDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
AttachmentDatabase partsDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
MentionDatabase mentionDatabase = DatabaseFactory.getMentionDatabase(context);
|
||||
|
||||
boolean mentionsSelf = Stream.of(mentions).filter(m -> Recipient.resolved(m.getRecipientId()).isLocalNumber()).findFirst().isPresent();
|
||||
|
||||
List<Attachment> allAttachments = new LinkedList<>();
|
||||
List<Attachment> contactAttachments = Stream.of(sharedContacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList();
|
||||
|
@ -1219,11 +1247,14 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
|
||||
contentValues.put(BODY, body);
|
||||
contentValues.put(PART_COUNT, allAttachments.size());
|
||||
contentValues.put(MENTIONS_SELF, mentionsSelf ? 1 : 0);
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
long messageId = db.insert(TABLE_NAME, null, contentValues);
|
||||
|
||||
mentionDatabase.insert(threadId, messageId, mentions);
|
||||
|
||||
Map<Attachment, AttachmentId> insertedAttachments = partsDatabase.insertAttachmentsForMessage(messageId, allAttachments, quoteAttachments);
|
||||
String serializedContacts = getSerializedSharedContacts(insertedAttachments, sharedContacts);
|
||||
String serializedPreviews = getSerializedLinkPreviews(insertedAttachments, linkPreviews);
|
||||
|
@ -1468,6 +1499,12 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
}
|
||||
}
|
||||
|
||||
private @NonNull List<Mention> parseQuoteMentions(Cursor cursor) {
|
||||
byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(QUOTE_MENTIONS));
|
||||
|
||||
return MentionUtil.bodyRangeListToMentions(context, raw);
|
||||
}
|
||||
|
||||
public void beginTransaction() {
|
||||
databaseHelper.getWritableDatabase().beginTransaction();
|
||||
}
|
||||
|
@ -1542,6 +1579,16 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
public MessageRecord getCurrent() {
|
||||
SlideDeck slideDeck = new SlideDeck(context, message.getAttachments());
|
||||
|
||||
CharSequence quoteText = message.getOutgoingQuote() != null ? message.getOutgoingQuote().getText() : null;
|
||||
List<Mention> quoteMentions = message.getOutgoingQuote() != null ? message.getOutgoingQuote().getMentions() : Collections.emptyList();
|
||||
|
||||
if (quoteText != null && !quoteMentions.isEmpty()) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions);
|
||||
|
||||
quoteText = updated.getBody();
|
||||
quoteMentions = updated.getMentions();
|
||||
}
|
||||
|
||||
return new MediaMmsMessageRecord(id,
|
||||
message.getRecipient(),
|
||||
message.getRecipient(),
|
||||
|
@ -1564,14 +1611,16 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
message.getOutgoingQuote() != null ?
|
||||
new Quote(message.getOutgoingQuote().getId(),
|
||||
message.getOutgoingQuote().getAuthor(),
|
||||
message.getOutgoingQuote().getText(),
|
||||
quoteText,
|
||||
message.getOutgoingQuote().isOriginalMissing(),
|
||||
new SlideDeck(context, message.getOutgoingQuote().getAttachments())) :
|
||||
new SlideDeck(context, message.getOutgoingQuote().getAttachments()),
|
||||
quoteMentions) :
|
||||
null,
|
||||
message.getSharedContacts(),
|
||||
message.getLinkPreviews(),
|
||||
false,
|
||||
Collections.emptyList(),
|
||||
false,
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
@ -1665,6 +1714,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
boolean isViewOnce = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) == 1;
|
||||
boolean remoteDelete = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.REMOTE_DELETED)) == 1;
|
||||
List<ReactionRecord> reactions = parseReactions(cursor);
|
||||
boolean mentionsSelf = CursorUtil.requireBoolean(cursor, MENTIONS_SELF);
|
||||
|
||||
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
||||
readReceiptCount = 0;
|
||||
|
@ -1686,7 +1736,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
threadId, body, slideDeck, partCount, box, mismatches,
|
||||
networkFailures, subscriptionId, expiresIn, expireStarted,
|
||||
isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, reactions,
|
||||
remoteDelete);
|
||||
remoteDelete, mentionsSelf);
|
||||
}
|
||||
|
||||
private List<IdentityKeyMismatch> getMismatchedIdentities(String document) {
|
||||
|
@ -1724,14 +1774,22 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
private @Nullable Quote getQuote(@NonNull Cursor cursor) {
|
||||
long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_ID));
|
||||
long quoteAuthor = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_AUTHOR));
|
||||
String quoteText = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_BODY));
|
||||
CharSequence quoteText = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_BODY));
|
||||
boolean quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_MISSING)) == 1;
|
||||
List<Mention> quoteMentions = parseQuoteMentions(cursor);
|
||||
List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachment(cursor);
|
||||
List<? extends Attachment> quoteAttachments = Stream.of(attachments).filter(Attachment::isQuote).toList();
|
||||
SlideDeck quoteDeck = new SlideDeck(context, quoteAttachments);
|
||||
|
||||
if (quoteId > 0 && quoteAuthor > 0) {
|
||||
return new Quote(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteDeck);
|
||||
if (quoteText != null && !quoteMentions.isEmpty()) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions);
|
||||
|
||||
quoteText = updated.getBody();
|
||||
quoteMentions = updated.getMentions();
|
||||
}
|
||||
|
||||
return new Quote(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteDeck, quoteMentions);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -82,6 +82,7 @@ public class MmsSmsDatabase extends Database {
|
|||
MmsDatabase.QUOTE_BODY,
|
||||
MmsDatabase.QUOTE_MISSING,
|
||||
MmsDatabase.QUOTE_ATTACHMENT,
|
||||
MmsDatabase.QUOTE_MENTIONS,
|
||||
MmsDatabase.SHARED_CONTACTS,
|
||||
MmsDatabase.LINK_PREVIEWS,
|
||||
MmsDatabase.VIEW_ONCE,
|
||||
|
@ -89,7 +90,8 @@ public class MmsSmsDatabase extends Database {
|
|||
MmsSmsColumns.REACTIONS,
|
||||
MmsSmsColumns.REACTIONS_UNREAD,
|
||||
MmsSmsColumns.REACTIONS_LAST_SEEN,
|
||||
MmsSmsColumns.REMOTE_DELETED};
|
||||
MmsSmsColumns.REMOTE_DELETED,
|
||||
MmsDatabase.MENTIONS_SELF};
|
||||
|
||||
public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
|
@ -408,6 +410,7 @@ public class MmsSmsDatabase extends Database {
|
|||
MmsDatabase.QUOTE_BODY,
|
||||
MmsDatabase.QUOTE_MISSING,
|
||||
MmsDatabase.QUOTE_ATTACHMENT,
|
||||
MmsDatabase.QUOTE_MENTIONS,
|
||||
MmsDatabase.SHARED_CONTACTS,
|
||||
MmsDatabase.LINK_PREVIEWS,
|
||||
MmsDatabase.VIEW_ONCE,
|
||||
|
@ -415,7 +418,8 @@ public class MmsSmsDatabase extends Database {
|
|||
MmsSmsColumns.REACTIONS_UNREAD,
|
||||
MmsSmsColumns.REACTIONS_LAST_SEEN,
|
||||
MmsSmsColumns.DATE_SERVER,
|
||||
MmsSmsColumns.REMOTE_DELETED };
|
||||
MmsSmsColumns.REMOTE_DELETED,
|
||||
MmsDatabase.MENTIONS_SELF };
|
||||
|
||||
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
|
||||
SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
|
||||
|
@ -440,6 +444,7 @@ public class MmsSmsDatabase extends Database {
|
|||
MmsDatabase.QUOTE_BODY,
|
||||
MmsDatabase.QUOTE_MISSING,
|
||||
MmsDatabase.QUOTE_ATTACHMENT,
|
||||
MmsDatabase.QUOTE_MENTIONS,
|
||||
MmsDatabase.SHARED_CONTACTS,
|
||||
MmsDatabase.LINK_PREVIEWS,
|
||||
MmsDatabase.VIEW_ONCE,
|
||||
|
@ -447,7 +452,8 @@ public class MmsSmsDatabase extends Database {
|
|||
MmsSmsColumns.REACTIONS_UNREAD,
|
||||
MmsSmsColumns.REACTIONS_LAST_SEEN,
|
||||
MmsSmsColumns.DATE_SERVER,
|
||||
MmsSmsColumns.REMOTE_DELETED };
|
||||
MmsSmsColumns.REMOTE_DELETED,
|
||||
MmsDatabase.MENTIONS_SELF };
|
||||
|
||||
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
|
||||
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
|
||||
|
@ -493,6 +499,7 @@ public class MmsSmsDatabase extends Database {
|
|||
mmsColumnsPresent.add(MmsDatabase.QUOTE_BODY);
|
||||
mmsColumnsPresent.add(MmsDatabase.QUOTE_MISSING);
|
||||
mmsColumnsPresent.add(MmsDatabase.QUOTE_ATTACHMENT);
|
||||
mmsColumnsPresent.add(MmsDatabase.QUOTE_MENTIONS);
|
||||
mmsColumnsPresent.add(MmsDatabase.SHARED_CONTACTS);
|
||||
mmsColumnsPresent.add(MmsDatabase.LINK_PREVIEWS);
|
||||
mmsColumnsPresent.add(MmsDatabase.VIEW_ONCE);
|
||||
|
@ -500,6 +507,7 @@ public class MmsSmsDatabase extends Database {
|
|||
mmsColumnsPresent.add(MmsDatabase.REACTIONS_UNREAD);
|
||||
mmsColumnsPresent.add(MmsDatabase.REACTIONS_LAST_SEEN);
|
||||
mmsColumnsPresent.add(MmsDatabase.REMOTE_DELETED);
|
||||
mmsColumnsPresent.add(MmsDatabase.MENTIONS_SELF);
|
||||
|
||||
Set<String> smsColumnsPresent = new HashSet<>();
|
||||
smsColumnsPresent.add(MmsSmsColumns.ID);
|
||||
|
|
|
@ -119,13 +119,13 @@ public class RecipientDatabase extends Database {
|
|||
private static final String PROFILE_GIVEN_NAME = "signal_profile_name";
|
||||
private static final String PROFILE_FAMILY_NAME = "profile_family_name";
|
||||
private static final String PROFILE_JOINED_NAME = "profile_joined_name";
|
||||
private static final String MENTION_SETTING = "mention_setting";
|
||||
|
||||
public static final String SEARCH_PROFILE_NAME = "search_signal_profile";
|
||||
private static final String SORT_NAME = "sort_name";
|
||||
private static final String IDENTITY_STATUS = "identity_status";
|
||||
private static final String IDENTITY_KEY = "identity_key";
|
||||
|
||||
|
||||
private static final String[] RECIPIENT_PROJECTION = new String[] {
|
||||
UUID, USERNAME, PHONE, EMAIL, GROUP_ID, GROUP_TYPE,
|
||||
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
|
||||
|
@ -136,7 +136,8 @@ public class RecipientDatabase extends Database {
|
|||
UNIDENTIFIED_ACCESS_MODE,
|
||||
FORCE_SMS_SELECTION,
|
||||
UUID_CAPABILITY, GROUPS_V2_CAPABILITY,
|
||||
STORAGE_SERVICE_ID, DIRTY
|
||||
STORAGE_SERVICE_ID, DIRTY,
|
||||
MENTION_SETTING
|
||||
};
|
||||
|
||||
private static final String[] ID_PROJECTION = new String[]{ID};
|
||||
|
@ -146,6 +147,8 @@ public class RecipientDatabase extends Database {
|
|||
.map(columnName -> TABLE_NAME + "." + columnName)
|
||||
.toList().toArray(new String[0]);
|
||||
|
||||
private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME};
|
||||
|
||||
private static final String[] RECIPIENT_FULL_PROJECTION = ArrayUtils.concat(
|
||||
new String[] { TABLE_NAME + "." + ID },
|
||||
TYPED_RECIPIENT_PROJECTION,
|
||||
|
@ -275,6 +278,24 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public enum MentionSetting {
|
||||
GLOBAL(0), ALWAYS_NOTIFY(1), DO_NOT_NOTIFY(2);
|
||||
|
||||
private final int id;
|
||||
|
||||
MentionSetting(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public static MentionSetting fromId(int id) {
|
||||
return values()[id];
|
||||
}
|
||||
}
|
||||
|
||||
public static final String CREATE_TABLE =
|
||||
"CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
UUID + " TEXT UNIQUE DEFAULT NULL, " +
|
||||
|
@ -314,7 +335,8 @@ public class RecipientDatabase extends Database {
|
|||
UUID_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " +
|
||||
GROUPS_V2_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " +
|
||||
STORAGE_SERVICE_ID + " TEXT UNIQUE DEFAULT NULL, " +
|
||||
DIRTY + " INTEGER DEFAULT " + DirtyState.CLEAN.getId() + ");";
|
||||
DIRTY + " INTEGER DEFAULT " + DirtyState.CLEAN.getId() + ", " +
|
||||
MENTION_SETTING + " INTEGER DEFAULT " + MentionSetting.GLOBAL.getId() + ");";
|
||||
|
||||
private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID +
|
||||
" FROM " + TABLE_NAME +
|
||||
|
@ -1078,6 +1100,7 @@ public class RecipientDatabase extends Database {
|
|||
int uuidCapabilityValue = CursorUtil.requireInt(cursor, UUID_CAPABILITY);
|
||||
int groupsV2CapabilityValue = CursorUtil.requireInt(cursor, GROUPS_V2_CAPABILITY);
|
||||
String storageKeyRaw = CursorUtil.requireString(cursor, STORAGE_SERVICE_ID);
|
||||
int mentionSettingId = CursorUtil.requireInt(cursor, MENTION_SETTING);
|
||||
|
||||
Optional<String> identityKeyRaw = CursorUtil.getString(cursor, IDENTITY_KEY);
|
||||
Optional<Integer> identityStatusRaw = CursorUtil.getInt(cursor, IDENTITY_STATUS);
|
||||
|
@ -1145,7 +1168,7 @@ public class RecipientDatabase extends Database {
|
|||
Recipient.Capability.deserialize(uuidCapabilityValue),
|
||||
Recipient.Capability.deserialize(groupsV2CapabilityValue),
|
||||
InsightsBannerTier.fromId(insightsBannerTier),
|
||||
storageKey, identityKey, identityStatus);
|
||||
storageKey, identityKey, identityStatus, MentionSetting.fromId(mentionSettingId));
|
||||
}
|
||||
|
||||
public BulkOperationsHandle beginBulkSystemContactUpdate() {
|
||||
|
@ -1299,6 +1322,14 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public void setMentionSetting(@NonNull RecipientId id, @NonNull MentionSetting mentionSetting) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(MENTION_SETTING, mentionSetting.getId());
|
||||
if (update(id, values)) {
|
||||
Recipient.live(id).refresh();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the profile key.
|
||||
* <p>
|
||||
|
@ -1902,6 +1933,29 @@ public class RecipientDatabase extends Database {
|
|||
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null);
|
||||
}
|
||||
|
||||
public @NonNull List<Recipient> queryRecipientsForMentions(@NonNull String query, @NonNull List<RecipientId> recipientIds) {
|
||||
if (TextUtils.isEmpty(query) || recipientIds.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
query = buildCaseInsensitiveGlobPattern(query);
|
||||
|
||||
String ids = TextUtils.join(",", Stream.of(recipientIds).map(RecipientId::serialize).toList());
|
||||
|
||||
String selection = BLOCKED + " = 0 AND " +
|
||||
ID + " IN (" + ids + ") AND " +
|
||||
SORT_NAME + " GLOB ?";
|
||||
|
||||
List<Recipient> recipients = new ArrayList<>();
|
||||
try (RecipientDatabase.RecipientReader reader = new RecipientReader(databaseHelper.getReadableDatabase().query(TABLE_NAME, MENTION_SEARCH_PROJECTION, selection, SqlUtil.buildArgs(query), null, null, SORT_NAME))) {
|
||||
Recipient recipient;
|
||||
while ((recipient = reader.getNext()) != null) {
|
||||
recipients.add(recipient);
|
||||
}
|
||||
}
|
||||
return recipients;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a case-insensitive GLOB pattern for fuzzy text queries. Works with all unicode
|
||||
* characters.
|
||||
|
@ -2384,6 +2438,10 @@ public class RecipientDatabase extends Database {
|
|||
return "NULLIF(" + column + ", '')";
|
||||
}
|
||||
|
||||
private static @NonNull String removeWhitespace(@NonNull String column) {
|
||||
return "REPLACE(" + column + ", ' ', '')";
|
||||
}
|
||||
|
||||
public interface ColorUpdater {
|
||||
MaterialColor update(@NonNull String name, @Nullable String color);
|
||||
}
|
||||
|
@ -2427,6 +2485,7 @@ public class RecipientDatabase extends Database {
|
|||
private final byte[] storageId;
|
||||
private final byte[] identityKey;
|
||||
private final IdentityDatabase.VerifiedStatus identityStatus;
|
||||
private final MentionSetting mentionSetting;
|
||||
|
||||
RecipientSettings(@NonNull RecipientId id,
|
||||
@Nullable UUID uuid,
|
||||
|
@ -2465,7 +2524,8 @@ public class RecipientDatabase extends Database {
|
|||
@NonNull InsightsBannerTier insightsBannerTier,
|
||||
@Nullable byte[] storageId,
|
||||
@Nullable byte[] identityKey,
|
||||
@NonNull IdentityDatabase.VerifiedStatus identityStatus)
|
||||
@NonNull IdentityDatabase.VerifiedStatus identityStatus,
|
||||
@NonNull MentionSetting mentionSetting)
|
||||
{
|
||||
this.id = id;
|
||||
this.uuid = uuid;
|
||||
|
@ -2505,6 +2565,7 @@ public class RecipientDatabase extends Database {
|
|||
this.storageId = storageId;
|
||||
this.identityKey = identityKey;
|
||||
this.identityStatus = identityStatus;
|
||||
this.mentionSetting = mentionSetting;
|
||||
}
|
||||
|
||||
public RecipientId getId() {
|
||||
|
@ -2661,6 +2722,10 @@ public class RecipientDatabase extends Database {
|
|||
public @NonNull IdentityDatabase.VerifiedStatus getIdentityStatus() {
|
||||
return identityStatus;
|
||||
}
|
||||
|
||||
public @NonNull MentionSetting getMentionSetting() {
|
||||
return mentionSetting;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RecipientReader implements Closeable {
|
||||
|
|
|
@ -66,7 +66,7 @@ public final class ThreadBodyUtil {
|
|||
return context.getString(R.string.ThreadRecord_media_message);
|
||||
} else {
|
||||
Log.w(TAG, "Got a media message with a body of a type we were not able to process. [contains media slide]:" + record.containsMediaSlide());
|
||||
return record.getBody();
|
||||
return getBody(context, record);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -75,10 +75,10 @@ public final class ThreadBodyUtil {
|
|||
}
|
||||
|
||||
private static @NonNull String getBodyOrDefault(@NonNull Context context, @NonNull MessageRecord record, @StringRes int defaultStringRes) {
|
||||
if (TextUtils.isEmpty(record.getBody())) {
|
||||
return context.getString(defaultStringRes);
|
||||
} else {
|
||||
return record.getBody();
|
||||
}
|
||||
return TextUtils.isEmpty(record.getBody()) ? context.getString(defaultStringRes) : getBody(context, record);
|
||||
}
|
||||
|
||||
private static @NonNull String getBody(@NonNull Context context, @NonNull MessageRecord record) {
|
||||
return MentionUtil.updateBodyWithDisplayNames(context, record, record.getBody()).toString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import net.sqlcipher.database.SQLiteDatabaseHook;
|
|||
import net.sqlcipher.database.SQLiteOpenHelper;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy;
|
||||
import org.thoughtcrime.securesms.database.MentionDatabase;
|
||||
import org.thoughtcrime.securesms.database.RemappedRecordsDatabase;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
|
@ -139,8 +140,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
private static final int QUOTE_CLEANUP = 65;
|
||||
private static final int BORDERLESS = 66;
|
||||
private static final int REMAPPED_RECORDS = 67;
|
||||
private static final int MENTIONS = 68;
|
||||
|
||||
private static final int DATABASE_VERSION = 67;
|
||||
private static final int DATABASE_VERSION = 68;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
|
@ -184,6 +186,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
db.execSQL(StorageKeyDatabase.CREATE_TABLE);
|
||||
db.execSQL(KeyValueDatabase.CREATE_TABLE);
|
||||
db.execSQL(MegaphoneDatabase.CREATE_TABLE);
|
||||
db.execSQL(MentionDatabase.CREATE_TABLE);
|
||||
executeStatements(db, SearchDatabase.CREATE_TABLE);
|
||||
executeStatements(db, JobDatabase.CREATE_TABLE);
|
||||
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE);
|
||||
|
@ -198,6 +201,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);
|
||||
executeStatements(db, StickerDatabase.CREATE_INDEXES);
|
||||
executeStatements(db, StorageKeyDatabase.CREATE_INDEXES);
|
||||
executeStatements(db, MentionDatabase.CREATE_INDEXES);
|
||||
|
||||
if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) {
|
||||
ClassicOpenHelper legacyHelper = new ClassicOpenHelper(context);
|
||||
|
@ -970,6 +974,23 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
|||
"new_id INTEGER)");
|
||||
}
|
||||
|
||||
if (oldVersion < MENTIONS) {
|
||||
db.execSQL("CREATE TABLE mention (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"thread_id INTEGER, " +
|
||||
"message_id INTEGER, " +
|
||||
"recipient_id INTEGER, " +
|
||||
"range_start INTEGER, " +
|
||||
"range_length INTEGER)");
|
||||
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS mention_message_id_index ON mention (message_id)");
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS mention_recipient_id_thread_id_index ON mention (recipient_id, thread_id);");
|
||||
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN quote_mentions BLOB DEFAULT NULL");
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN mentions_self INTEGER DEFAULT 0");
|
||||
|
||||
db.execSQL("ALTER TABLE recipient ADD COLUMN mention_setting INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
|
|
@ -45,6 +45,7 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
|||
private final static String TAG = MediaMmsMessageRecord.class.getSimpleName();
|
||||
|
||||
private final int partCount;
|
||||
private final boolean mentionsSelf;
|
||||
|
||||
public MediaMmsMessageRecord(long id,
|
||||
Recipient conversationRecipient,
|
||||
|
@ -71,19 +72,26 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
|||
@NonNull List<LinkPreview> linkPreviews,
|
||||
boolean unidentified,
|
||||
@NonNull List<ReactionRecord> reactions,
|
||||
boolean remoteDelete)
|
||||
boolean remoteDelete,
|
||||
boolean mentionsSelf)
|
||||
{
|
||||
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent,
|
||||
dateReceived, dateServer, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
|
||||
subscriptionId, expiresIn, expireStarted, viewOnce, slideDeck,
|
||||
readReceiptCount, quote, contacts, linkPreviews, unidentified, reactions, remoteDelete);
|
||||
this.partCount = partCount;
|
||||
this.partCount = partCount;
|
||||
this.mentionsSelf = mentionsSelf;
|
||||
}
|
||||
|
||||
public int getPartCount() {
|
||||
return partCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSelfMention() {
|
||||
return mentionsSelf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMmsNotification() {
|
||||
return false;
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class Mention implements Comparable<Mention>, Parcelable {
|
||||
private final RecipientId recipientId;
|
||||
private final int start;
|
||||
private final int length;
|
||||
|
||||
public Mention(@NonNull RecipientId recipientId, int start, int length) {
|
||||
this.recipientId = recipientId;
|
||||
this.start = start;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
protected Mention(Parcel in) {
|
||||
recipientId = in.readParcelable(RecipientId.class.getClassLoader());
|
||||
start = in.readInt();
|
||||
length = in.readInt();
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getRecipientId() {
|
||||
return recipientId;
|
||||
}
|
||||
|
||||
public int getStart() {
|
||||
return start;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(Mention other) {
|
||||
return Integer.compare(start, other.start);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(recipientId, start, length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object object) {
|
||||
if (this == object) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (object == null || getClass() != object.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Mention that = (Mention) object;
|
||||
return recipientId.equals(that.recipientId) && start == that.start && length == that.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeParcelable(recipientId, flags);
|
||||
dest.writeInt(start);
|
||||
dest.writeInt(length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static final Creator<Mention> CREATOR = new Creator<Mention>() {
|
||||
@Override
|
||||
public Mention createFromParcel(Parcel in) {
|
||||
return new Mention(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mention[] newArray(int size) {
|
||||
return new Mention[size];
|
||||
}
|
||||
};
|
||||
}
|
|
@ -374,4 +374,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
public @NonNull List<ReactionRecord> getReactions() {
|
||||
return reactions;
|
||||
}
|
||||
|
||||
public boolean hasSelfMention() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,42 @@
|
|||
package org.thoughtcrime.securesms.database.model;
|
||||
|
||||
import android.text.SpannableString;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class Quote {
|
||||
|
||||
private final long id;
|
||||
private final RecipientId author;
|
||||
private final String text;
|
||||
private final boolean missing;
|
||||
private final SlideDeck attachment;
|
||||
private final long id;
|
||||
private final RecipientId author;
|
||||
private final CharSequence text;
|
||||
private final boolean missing;
|
||||
private final SlideDeck attachment;
|
||||
private final List<Mention> mentions;
|
||||
|
||||
public Quote(long id, @NonNull RecipientId author, @Nullable String text, boolean missing, @NonNull SlideDeck attachment) {
|
||||
this.id = id;
|
||||
this.author = author;
|
||||
this.text = text;
|
||||
this.missing = missing;
|
||||
this.attachment = attachment;
|
||||
public Quote(long id,
|
||||
@NonNull RecipientId author,
|
||||
@Nullable CharSequence text,
|
||||
boolean missing,
|
||||
@NonNull SlideDeck attachment,
|
||||
@NonNull List<Mention> mentions)
|
||||
{
|
||||
this.id = id;
|
||||
this.author = author;
|
||||
this.missing = missing;
|
||||
this.attachment = attachment;
|
||||
this.mentions = mentions;
|
||||
|
||||
SpannableString spannable = new SpannableString(text);
|
||||
MentionAnnotation.setMentionAnnotations(spannable, mentions);
|
||||
|
||||
this.text = spannable;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
|
@ -31,7 +47,7 @@ public class Quote {
|
|||
return author;
|
||||
}
|
||||
|
||||
public @Nullable String getText() {
|
||||
public @Nullable CharSequence getDisplayText() {
|
||||
return text;
|
||||
}
|
||||
|
||||
|
|
|
@ -149,7 +149,7 @@ final class GroupManagerV1 {
|
|||
avatarAttachment = new UriAttachment(avatarUri, MediaUtil.IMAGE_PNG, AttachmentDatabase.TRANSFER_PROGRESS_DONE, avatar.length, null, false, false, false, null, null, null, null, null);
|
||||
}
|
||||
|
||||
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList());
|
||||
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, groupContext, avatarAttachment, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
long threadId = MessageSender.send(context, outgoingMessage, -1, false, null);
|
||||
|
||||
return new GroupActionResult(groupRecipient, threadId, newMemberCount, Collections.emptyList());
|
||||
|
@ -241,6 +241,7 @@ final class GroupManagerV1 {
|
|||
false,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -519,6 +519,7 @@ final class GroupManagerV2 {
|
|||
false,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
if (plainGroupChange != null && DecryptedGroupUtil.changeIsEmptyExceptForProfileKeyChanges(plainGroupChange)) {
|
||||
|
|
|
@ -240,7 +240,7 @@ public final class GroupV1MessageProcessor {
|
|||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
RecipientId recipientId = DatabaseFactory.getRecipientDatabase(context).getOrInsertFromGroupId(GroupId.v1orThrow(group.getGroupId()));
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(recipient, storage, null, content.getTimestamp(), 0, false, null, Collections.emptyList(), Collections.emptyList());
|
||||
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(recipient, storage, null, content.getTimestamp(), 0, false, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
|
||||
|
||||
|
|
|
@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId;
|
|||
import org.thoughtcrime.securesms.recipients.ui.bottomsheet.RecipientBottomSheetDialogFragment;
|
||||
import org.thoughtcrime.securesms.recipients.ui.notifications.CustomNotificationsDialogFragment;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.LifecycleCursorWrapper;
|
||||
import org.thoughtcrime.securesms.util.views.LearnMoreTextView;
|
||||
|
||||
|
@ -99,6 +100,8 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
private TextView muteNotificationsUntilLabel;
|
||||
private TextView customNotificationsButton;
|
||||
private View customNotificationsRow;
|
||||
private View mentionsRow;
|
||||
private TextView mentionsValue;
|
||||
private View toggleAllMembers;
|
||||
|
||||
private final Recipient.FallbackPhotoProvider fallbackPhotoProvider = new Recipient.FallbackPhotoProvider() {
|
||||
|
@ -156,6 +159,8 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
muteNotificationsRow = view.findViewById(R.id.group_mute_notifications_row);
|
||||
customNotificationsButton = view.findViewById(R.id.group_custom_notifications_button);
|
||||
customNotificationsRow = view.findViewById(R.id.group_custom_notifications_row);
|
||||
mentionsRow = view.findViewById(R.id.group_mentions_row);
|
||||
mentionsValue = view.findViewById(R.id.group_mentions_value);
|
||||
toggleAllMembers = view.findViewById(R.id.toggle_all_members);
|
||||
|
||||
groupV1Indicator.setOnLinkClickListener(v -> GroupsLearnMoreBottomSheetDialogFragment.show(requireFragmentManager()));
|
||||
|
@ -317,7 +322,6 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
|
||||
customNotificationsRow.setVisibility(View.VISIBLE);
|
||||
|
||||
//noinspection CodeBlock2Expr
|
||||
if (NotificationChannels.supported()) {
|
||||
viewModel.hasCustomNotifications().observe(getViewLifecycleOwner(), hasCustomNotifications -> {
|
||||
customNotificationsButton.setText(hasCustomNotifications ? R.string.ManageGroupActivity_on
|
||||
|
@ -325,6 +329,10 @@ public class ManageGroupFragment extends LoggingFragment {
|
|||
});
|
||||
}
|
||||
|
||||
mentionsRow.setVisibility(FeatureFlags.mentions() && groupId.isV2() ? View.VISIBLE : View.GONE);
|
||||
mentionsRow.setOnClickListener(v -> viewModel.handleMentionNotificationSelection());
|
||||
viewModel.getMentionSetting().observe(getViewLifecycleOwner(), value -> mentionsValue.setText(value));
|
||||
|
||||
viewModel.getSnackbarEvents().observe(getViewLifecycleOwner(), this::handleSnackbarEvent);
|
||||
viewModel.getInvitedDialogEvents().observe(getViewLifecycleOwner(), this::handleInvitedDialogEvent);
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
|||
import org.thoughtcrime.securesms.ContactSelectionListFragment;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.groups.GroupAccessControl;
|
||||
import org.thoughtcrime.securesms.groups.GroupChangeException;
|
||||
|
@ -152,6 +153,13 @@ final class ManageGroupRepository {
|
|||
});
|
||||
}
|
||||
|
||||
void setMentionSetting(RecipientDatabase.MentionSetting mentionSetting) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
RecipientId recipientId = Recipient.externalGroup(context, groupId).getId();
|
||||
DatabaseFactory.getRecipientDatabase(context).setMentionSetting(recipientId, mentionSetting);
|
||||
});
|
||||
}
|
||||
|
||||
static final class GroupStateResult {
|
||||
|
||||
private final long threadId;
|
||||
|
|
|
@ -22,6 +22,8 @@ import org.thoughtcrime.securesms.ExpirationDialog;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.contacts.ContactsCursorLoader;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.MediaLoader;
|
||||
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
|
||||
import org.thoughtcrime.securesms.groups.GroupAccessControl;
|
||||
|
@ -31,6 +33,7 @@ import org.thoughtcrime.securesms.groups.ui.GroupChangeFailureReason;
|
|||
import org.thoughtcrime.securesms.groups.ui.GroupErrors;
|
||||
import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry;
|
||||
import org.thoughtcrime.securesms.groups.ui.addmembers.AddMembersActivity;
|
||||
import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupMentionSettingDialog;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
@ -75,6 +78,7 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
private final LiveData<Boolean> canLeaveGroup;
|
||||
private final LiveData<Boolean> canBlockGroup;
|
||||
private final LiveData<Boolean> showLegacyIndicator;
|
||||
private final LiveData<String> mentionSetting;
|
||||
|
||||
private ManageGroupViewModel(@NonNull Context context, @NonNull ManageGroupRepository manageGroupRepository) {
|
||||
this.context = context;
|
||||
|
@ -114,6 +118,8 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
recipient -> recipient.getNotificationChannel() != null || !NotificationChannels.supported());
|
||||
this.canLeaveGroup = liveGroup.isActive();
|
||||
this.canBlockGroup = Transformations.map(this.groupRecipient, recipient -> !recipient.isBlocked());
|
||||
this.mentionSetting = Transformations.distinctUntilChanged(Transformations.map(this.groupRecipient,
|
||||
recipient -> MentionUtil.getMentionSettingDisplayValue(context, recipient.getMentionSetting())));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -207,6 +213,10 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
return canLeaveGroup;
|
||||
}
|
||||
|
||||
LiveData<String> getMentionSetting() {
|
||||
return mentionSetting;
|
||||
}
|
||||
|
||||
void handleExpirationSelection() {
|
||||
manageGroupRepository.getRecipient(groupRecipient ->
|
||||
ExpirationDialog.show(context,
|
||||
|
@ -250,6 +260,10 @@ public class ManageGroupViewModel extends ViewModel {
|
|||
memberListCollapseState.setValue(CollapseState.OPEN);
|
||||
}
|
||||
|
||||
void handleMentionNotificationSelection() {
|
||||
manageGroupRepository.getRecipient(r -> GroupMentionSettingDialog.show(context, r.getMentionSetting(), mentionSetting -> manageGroupRepository.setMentionSetting(mentionSetting)));
|
||||
}
|
||||
|
||||
private void onBlockAndLeaveConfirmed() {
|
||||
SimpleProgressDialog.DismissibleDialog dismissibleDialog = SimpleProgressDialog.showDelayed(context);
|
||||
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
package org.thoughtcrime.securesms.groups.ui.managegroup.dialogs;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.CheckedTextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.MentionSetting;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
|
||||
public final class GroupMentionSettingDialog {
|
||||
|
||||
public static void show(@NonNull Context context, @NonNull MentionSetting mentionSetting, @Nullable Consumer<MentionSetting> callback) {
|
||||
SelectionCallback selectionCallback = new SelectionCallback(mentionSetting, callback);
|
||||
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.GroupMentionSettingDialog_notify_me_for_mentions)
|
||||
.setView(getView(context, mentionSetting, selectionCallback))
|
||||
.setPositiveButton(android.R.string.ok, selectionCallback)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@SuppressLint("InflateParams")
|
||||
private static View getView(@NonNull Context context, @NonNull MentionSetting mentionSetting, @NonNull SelectionCallback selectionCallback) {
|
||||
View root = LayoutInflater.from(context).inflate(R.layout.group_mention_setting_dialog, null, false);
|
||||
CheckedTextView defaultOption = root.findViewById(R.id.group_mention_setting_default);
|
||||
CheckedTextView alwaysNotify = root.findViewById(R.id.group_mention_setting_always_notify);
|
||||
CheckedTextView dontNotify = root.findViewById(R.id.group_mention_setting_dont_notify);
|
||||
|
||||
defaultOption.setText(SignalStore.notificationSettings().isMentionNotifiesMeEnabled() ? R.string.GroupMentionSettingDialog_default_notify_me
|
||||
: R.string.GroupMentionSettingDialog_default_dont_notify_me);
|
||||
|
||||
View.OnClickListener listener = (v) -> {
|
||||
defaultOption.setChecked(defaultOption == v);
|
||||
alwaysNotify.setChecked(alwaysNotify == v);
|
||||
dontNotify.setChecked(dontNotify == v);
|
||||
|
||||
if (defaultOption.isChecked()) selectionCallback.selection = MentionSetting.GLOBAL;
|
||||
else if (alwaysNotify.isChecked()) selectionCallback.selection = MentionSetting.ALWAYS_NOTIFY;
|
||||
else if (dontNotify.isChecked()) selectionCallback.selection = MentionSetting.DO_NOT_NOTIFY;
|
||||
};
|
||||
|
||||
defaultOption.setOnClickListener(listener);
|
||||
alwaysNotify.setOnClickListener(listener);
|
||||
dontNotify.setOnClickListener(listener);
|
||||
|
||||
switch (mentionSetting) {
|
||||
case GLOBAL:
|
||||
listener.onClick(defaultOption);
|
||||
break;
|
||||
case ALWAYS_NOTIFY:
|
||||
listener.onClick(alwaysNotify);
|
||||
break;
|
||||
case DO_NOT_NOTIFY:
|
||||
listener.onClick(dontNotify);
|
||||
break;
|
||||
}
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
private static class SelectionCallback implements DialogInterface.OnClickListener {
|
||||
|
||||
@NonNull private final MentionSetting previousMentionSetting;
|
||||
@NonNull private MentionSetting selection;
|
||||
@Nullable private final Consumer<MentionSetting> callback;
|
||||
|
||||
public SelectionCallback(@NonNull MentionSetting previousMentionSetting, @Nullable Consumer<MentionSetting> callback) {
|
||||
this.previousMentionSetting = previousMentionSetting;
|
||||
this.selection = previousMentionSetting;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
if (callback != null && selection != previousMentionSetting) {
|
||||
callback.accept(selection);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -232,6 +232,7 @@ public final class GroupsV2StateProcessor {
|
|||
false,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
try {
|
||||
|
@ -397,7 +398,7 @@ public final class GroupsV2StateProcessor {
|
|||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
RecipientId recipientId = recipientDatabase.getOrInsertFromGroupId(groupId);
|
||||
Recipient recipient = Recipient.resolved(recipientId);
|
||||
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(recipient, decryptedGroupV2Context, null, timestamp, 0, false, null, Collections.emptyList(), Collections.emptyList());
|
||||
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(recipient, decryptedGroupV2Context, null, timestamp, 0, false, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
long threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(recipient);
|
||||
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
|
||||
|
||||
|
|
|
@ -286,6 +286,7 @@ public final class PushGroupSendJob extends PushSendJob {
|
|||
Optional<SignalServiceDataMessage.Sticker> sticker = getStickerFor(message);
|
||||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
List<Preview> previews = getPreviewsFor(message);
|
||||
List<SignalServiceDataMessage.Mention> mentions = getMentionsFor(message.getMentions());
|
||||
List<SignalServiceAddress> addresses = Stream.of(destinations).map(this::getPushAddress).toList();
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
|
||||
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
|
||||
|
@ -352,6 +353,7 @@ public final class PushGroupSendJob extends PushSendJob {
|
|||
.withSticker(sticker.orNull())
|
||||
.withSharedContacts(sharedContacts)
|
||||
.withPreviews(previews)
|
||||
.withMentions(mentions)
|
||||
.build();
|
||||
|
||||
Log.i(TAG, JobLogger.format(this, "Beginning message send."));
|
||||
|
|
|
@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase;
|
|||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.StickerDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
|
@ -123,6 +124,7 @@ import org.whispersystems.signalservice.api.messages.multidevice.ViewOnceOpenMes
|
|||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
|
@ -133,6 +135,7 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class PushProcessMessageJob extends BaseJob {
|
||||
|
@ -341,7 +344,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
|
||||
if (content.getDataMessage().isPresent()) {
|
||||
SignalServiceDataMessage message = content.getDataMessage().get();
|
||||
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent();
|
||||
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent() || message.getMentions().isPresent();
|
||||
Optional<GroupId> groupId = GroupUtil.idFromGroupContext(message.getGroupContext());
|
||||
boolean isGv2Message = groupId.isPresent() && groupId.get().isV2();
|
||||
|
||||
|
@ -729,6 +732,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
Optional.absent());
|
||||
|
||||
database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
||||
|
@ -1029,6 +1033,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
Optional<QuoteModel> quote = getValidatedQuote(message.getQuote());
|
||||
Optional<List<Contact>> sharedContacts = getContacts(message.getSharedContacts());
|
||||
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().or(""));
|
||||
Optional<List<Mention>> mentions = getMentions(message.getMentions());
|
||||
Optional<Attachment> sticker = getStickerAttachment(message.getSticker());
|
||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(RecipientId.fromHighTrust(content.getSender()),
|
||||
message.getTimestamp(),
|
||||
|
@ -1044,6 +1049,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
quote,
|
||||
sharedContacts,
|
||||
linkPreviews,
|
||||
mentions,
|
||||
sticker);
|
||||
|
||||
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
||||
|
@ -1109,6 +1115,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
Optional<Attachment> sticker = getStickerAttachment(message.getMessage().getSticker());
|
||||
Optional<List<Contact>> sharedContacts = getContacts(message.getMessage().getSharedContacts());
|
||||
Optional<List<LinkPreview>> previews = getLinkPreviews(message.getMessage().getPreviews(), message.getMessage().getBody().or(""));
|
||||
Optional<List<Mention>> mentions = getMentions(message.getMessage().getMentions());
|
||||
boolean viewOnce = message.getMessage().isViewOnce();
|
||||
List<Attachment> syncAttachments = viewOnce ? Collections.singletonList(new TombstoneAttachment(MediaUtil.VIEW_ONCE, false))
|
||||
: PointerAttachment.forPointers(message.getMessage().getAttachments());
|
||||
|
@ -1125,6 +1132,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
ThreadDatabase.DistributionTypes.DEFAULT, quote.orNull(),
|
||||
sharedContacts.or(Collections.emptyList()),
|
||||
previews.or(Collections.emptyList()),
|
||||
mentions.or(Collections.emptyList()),
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
|
||||
mediaMessage = new OutgoingSecureMediaMessage(mediaMessage);
|
||||
|
@ -1290,7 +1298,18 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
long messageId;
|
||||
|
||||
if (isGroup) {
|
||||
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient, new SlideDeck(), body, message.getTimestamp(), -1, expiresInMillis, false, ThreadDatabase.DistributionTypes.DEFAULT, null, Collections.emptyList(), Collections.emptyList());
|
||||
OutgoingMediaMessage outgoingMediaMessage = new OutgoingMediaMessage(recipient,
|
||||
new SlideDeck(),
|
||||
body,
|
||||
message.getTimestamp(),
|
||||
-1,
|
||||
expiresInMillis,
|
||||
false,
|
||||
ThreadDatabase.DistributionTypes.DEFAULT,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
outgoingMediaMessage = new OutgoingSecureMediaMessage(outgoingMediaMessage);
|
||||
|
||||
messageId = DatabaseFactory.getMmsDatabase(context).insertMessageOutbox(outgoingMediaMessage, threadId, false, GroupReceiptDatabase.STATUS_UNKNOWN, null);
|
||||
|
@ -1577,10 +1596,13 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
Log.i(TAG, "Found matching message record...");
|
||||
|
||||
List<Attachment> attachments = new LinkedList<>();
|
||||
List<Mention> mentions = new LinkedList<>();
|
||||
|
||||
if (message.isMms()) {
|
||||
MmsMessageRecord mmsMessage = (MmsMessageRecord) message;
|
||||
|
||||
mentions.addAll(DatabaseFactory.getMentionDatabase(context).getMentionsForMessage(mmsMessage.getId()));
|
||||
|
||||
if (mmsMessage.isViewOnce()) {
|
||||
attachments.add(new TombstoneAttachment(MediaUtil.VIEW_ONCE, true));
|
||||
} else {
|
||||
|
@ -1595,7 +1617,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
}
|
||||
}
|
||||
|
||||
return Optional.of(new QuoteModel(quote.get().getId(), author, message.getBody(), false, attachments));
|
||||
return Optional.of(new QuoteModel(quote.get().getId(), author, message.getBody(), false, attachments, mentions));
|
||||
} else if (message != null) {
|
||||
Log.w(TAG, "Found the target for the quote, but it's flagged as remotely deleted.");
|
||||
}
|
||||
|
@ -1606,7 +1628,8 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
author,
|
||||
quote.get().getText(),
|
||||
true,
|
||||
PointerAttachment.forPointers(quote.get().getAttachments())));
|
||||
PointerAttachment.forPointers(quote.get().getAttachments()),
|
||||
getMentions(quote.get().getMentions())));
|
||||
}
|
||||
|
||||
private Optional<Attachment> getStickerAttachment(Optional<SignalServiceDataMessage.Sticker> sticker) {
|
||||
|
@ -1685,6 +1708,26 @@ public final class PushProcessMessageJob extends BaseJob {
|
|||
return Optional.of(linkPreviews);
|
||||
}
|
||||
|
||||
private Optional<List<Mention>> getMentions(Optional<List<SignalServiceDataMessage.Mention>> signalServiceMentions) {
|
||||
if (!signalServiceMentions.isPresent()) return Optional.absent();
|
||||
|
||||
return Optional.of(getMentions(signalServiceMentions.get()));
|
||||
}
|
||||
|
||||
private @NonNull List<Mention> getMentions(@Nullable List<SignalServiceDataMessage.Mention> signalServiceMentions) {
|
||||
if (signalServiceMentions == null || signalServiceMentions.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<Mention> mentions = new ArrayList<>(signalServiceMentions.size());
|
||||
|
||||
for (SignalServiceDataMessage.Mention mention : signalServiceMentions) {
|
||||
mentions.add(new Mention(Recipient.externalPush(context, mention.getUuid(), null, false).getId(), mention.getStart(), mention.getLength()));
|
||||
}
|
||||
|
||||
return mentions;
|
||||
}
|
||||
|
||||
private Optional<InsertResult> insertPlaceholder(@NonNull String sender, int senderDevice, long timestamp) {
|
||||
return insertPlaceholder(sender, senderDevice, timestamp, Optional.absent());
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.contactshare.Contact;
|
|||
import org.thoughtcrime.securesms.contactshare.ContactModelMapper;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
|
@ -239,6 +240,7 @@ public abstract class PushSendJob extends SendJob {
|
|||
long quoteId = message.getOutgoingQuote().getId();
|
||||
String quoteBody = message.getOutgoingQuote().getText();
|
||||
RecipientId quoteAuthor = message.getOutgoingQuote().getAuthor();
|
||||
List<SignalServiceDataMessage.Mention> quoteMentions = getMentionsFor(message.getOutgoingQuote().getMentions());
|
||||
List<SignalServiceDataMessage.Quote.QuotedAttachment> quoteAttachments = new LinkedList<>();
|
||||
List<Attachment> filteredAttachments = Stream.of(message.getOutgoingQuote().getAttachments())
|
||||
.filterNot(a -> MediaUtil.isViewOnceType(a.getContentType()))
|
||||
|
@ -284,7 +286,7 @@ public abstract class PushSendJob extends SendJob {
|
|||
|
||||
Recipient quoteAuthorRecipient = Recipient.resolved(quoteAuthor);
|
||||
SignalServiceAddress quoteAddress = RecipientUtil.toSignalServiceAddress(context, quoteAuthorRecipient);
|
||||
return Optional.of(new SignalServiceDataMessage.Quote(quoteId, quoteAddress, quoteBody, quoteAttachments));
|
||||
return Optional.of(new SignalServiceDataMessage.Quote(quoteId, quoteAddress, quoteBody, quoteAttachments, quoteMentions));
|
||||
}
|
||||
|
||||
protected Optional<SignalServiceDataMessage.Sticker> getStickerFor(OutgoingMediaMessage message) {
|
||||
|
@ -334,6 +336,12 @@ public abstract class PushSendJob extends SendJob {
|
|||
}).toList();
|
||||
}
|
||||
|
||||
List<SignalServiceDataMessage.Mention> getMentionsFor(@NonNull List<Mention> mentions) {
|
||||
return Stream.of(mentions)
|
||||
.map(m -> new SignalServiceDataMessage.Mention(Recipient.resolved(m.getRecipientId()).requireUuid(), m.getStart(), m.getLength()))
|
||||
.toList();
|
||||
}
|
||||
|
||||
protected void rotateSenderCertificateIfNecessary() throws IOException {
|
||||
try {
|
||||
byte[] certificateBytes = TextSecurePreferences.getUnidentifiedAccessCertificate(context);
|
||||
|
|
|
@ -98,7 +98,7 @@ public final class WakeGroupV2Job extends BaseJob {
|
|||
GroupDatabase.V2GroupProperties v2GroupProperties = group.get().requireV2GroupProperties();
|
||||
DecryptedGroupV2Context decryptedGroupV2Context = GroupProtoUtil.createDecryptedGroupV2Context(v2GroupProperties.getGroupMasterKey(), v2GroupProperties.getDecryptedGroup(), null, null);
|
||||
MmsDatabase mmsDatabase = DatabaseFactory.getMmsDatabase(context);
|
||||
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, decryptedGroupV2Context, null, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList());
|
||||
OutgoingGroupUpdateMessage outgoingMessage = new OutgoingGroupUpdateMessage(groupRecipient, decryptedGroupV2Context, null, System.currentTimeMillis(), 0, false, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
|
||||
long messageId = mmsDatabase.insertMessageOutbox(outgoingMessage, threadId, false, null);
|
||||
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class NotificationSettings extends SignalStoreValues {
|
||||
|
||||
public static final String MENTIONS_NOTIFY_ME = "notifications.mentions.notify_me";
|
||||
|
||||
NotificationSettings(@NonNull KeyValueStore store) {
|
||||
super(store);
|
||||
}
|
||||
|
||||
@Override
|
||||
void onFirstEverAppLaunch() {
|
||||
}
|
||||
|
||||
public boolean isMentionNotifiesMeEnabled() {
|
||||
return getBoolean(MENTIONS_NOTIFY_ME, true);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
package org.thoughtcrime.securesms.keyvalue;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.OnLifecycleEvent;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
|
@ -25,6 +24,7 @@ public final class SignalStore {
|
|||
private final MiscellaneousValues misc;
|
||||
private final InternalValues internalValues;
|
||||
private final EmojiValues emojiValues;
|
||||
private final NotificationSettings notificationSettings;
|
||||
|
||||
private SignalStore() {
|
||||
this.store = ApplicationDependencies.getKeyValueStore();
|
||||
|
@ -38,6 +38,7 @@ public final class SignalStore {
|
|||
this.misc = new MiscellaneousValues(store);
|
||||
this.internalValues = new InternalValues(store);
|
||||
this.emojiValues = new EmojiValues(store);
|
||||
this.notificationSettings = new NotificationSettings(store);
|
||||
}
|
||||
|
||||
public static void onFirstEverAppLaunch() {
|
||||
|
@ -50,6 +51,7 @@ public final class SignalStore {
|
|||
tooltips().onFirstEverAppLaunch();
|
||||
misc().onFirstEverAppLaunch();
|
||||
internalValues().onFirstEverAppLaunch();
|
||||
notificationSettings().onFirstEverAppLaunch();
|
||||
}
|
||||
|
||||
public static @NonNull KbsValues kbsValues() {
|
||||
|
@ -92,6 +94,10 @@ public final class SignalStore {
|
|||
return INSTANCE.emojiValues;
|
||||
}
|
||||
|
||||
public static @NonNull NotificationSettings notificationSettings() {
|
||||
return INSTANCE.notificationSettings;
|
||||
}
|
||||
|
||||
public static @NonNull GroupsV2AuthorizationSignalStoreCache groupsV2AuthorizationCache() {
|
||||
return new GroupsV2AuthorizationSignalStoreCache(getStore());
|
||||
}
|
||||
|
|
|
@ -1,28 +1,33 @@
|
|||
package org.thoughtcrime.securesms.longmessage;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
|
||||
/**
|
||||
* A wrapper around a {@link MessageRecord} and its extra text attachment expanded into a string
|
||||
* A wrapper around a {@link ConversationMessage} and its extra text attachment expanded into a string
|
||||
* held in memory.
|
||||
*/
|
||||
class LongMessage {
|
||||
|
||||
private final MessageRecord messageRecord;
|
||||
private final String fullBody;
|
||||
private final ConversationMessage conversationMessage;
|
||||
private final String fullBody;
|
||||
|
||||
LongMessage(MessageRecord messageRecord, String fullBody) {
|
||||
this.messageRecord = messageRecord;
|
||||
this.fullBody = fullBody;
|
||||
LongMessage(@NonNull ConversationMessage conversationMessage, @NonNull String fullBody) {
|
||||
this.conversationMessage = conversationMessage;
|
||||
this.fullBody = fullBody;
|
||||
}
|
||||
|
||||
MessageRecord getMessageRecord() {
|
||||
return messageRecord;
|
||||
@NonNull MessageRecord getMessageRecord() {
|
||||
return conversationMessage.getMessageRecord();
|
||||
}
|
||||
|
||||
String getFullBody() {
|
||||
return !TextUtils.isEmpty(fullBody) ? fullBody : messageRecord.getBody();
|
||||
@NonNull CharSequence getFullBody(@NonNull Context context) {
|
||||
return !TextUtils.isEmpty(fullBody) ? fullBody : conversationMessage.getDisplayBody(context);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -147,7 +147,7 @@ public class LongMessageActivity extends PassphraseRequiredActivity {
|
|||
TextView text = bubble.findViewById(R.id.longmessage_text);
|
||||
ConversationItemFooter footer = bubble.findViewById(R.id.longmessage_footer);
|
||||
|
||||
String trimmedBody = getTrimmedBody(message.get().getFullBody());
|
||||
CharSequence trimmedBody = getTrimmedBody(message.get().getFullBody(this));
|
||||
SpannableString styledBody = linkifyMessageBody(new SpannableString(trimmedBody));
|
||||
|
||||
bubble.setVisibility(View.VISIBLE);
|
||||
|
@ -158,9 +158,9 @@ public class LongMessageActivity extends PassphraseRequiredActivity {
|
|||
});
|
||||
}
|
||||
|
||||
private String getTrimmedBody(@NonNull String text) {
|
||||
private CharSequence getTrimmedBody(@NonNull CharSequence text) {
|
||||
return text.length() <= MAX_DISPLAY_LENGTH ? text
|
||||
: text.substring(0, MAX_DISPLAY_LENGTH);
|
||||
: text.subSequence(0, MAX_DISPLAY_LENGTH);
|
||||
}
|
||||
|
||||
private SpannableString linkifyMessageBody(SpannableString messageBody) {
|
||||
|
|
|
@ -6,6 +6,8 @@ import android.net.Uri;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
|
@ -38,7 +40,7 @@ class LongMessageRepository {
|
|||
if (isMms) {
|
||||
callback.onComplete(getMmsLongMessage(context, mmsDatabase, messageId));
|
||||
} else {
|
||||
callback.onComplete(getSmsLongMessage(smsDatabase, messageId));
|
||||
callback.onComplete(getSmsLongMessage(context, smsDatabase, messageId));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -51,9 +53,9 @@ class LongMessageRepository {
|
|||
TextSlide textSlide = record.get().getSlideDeck().getTextSlide();
|
||||
|
||||
if (textSlide != null && textSlide.getUri() != null) {
|
||||
return Optional.of(new LongMessage(record.get(), readFullBody(context, textSlide.getUri())));
|
||||
return Optional.of(new LongMessage(ConversationMessageFactory.createWithUnresolvedData(context, record.get()), readFullBody(context, textSlide.getUri())));
|
||||
} else {
|
||||
return Optional.of(new LongMessage(record.get(), ""));
|
||||
return Optional.of(new LongMessage(ConversationMessageFactory.createWithUnresolvedData(context, record.get()), ""));
|
||||
}
|
||||
} else {
|
||||
return Optional.absent();
|
||||
|
@ -61,11 +63,11 @@ class LongMessageRepository {
|
|||
}
|
||||
|
||||
@WorkerThread
|
||||
private Optional<LongMessage> getSmsLongMessage(@NonNull SmsDatabase smsDatabase, long messageId) {
|
||||
private Optional<LongMessage> getSmsLongMessage(@NonNull Context context, @NonNull SmsDatabase smsDatabase, long messageId) {
|
||||
Optional<MessageRecord> record = getSmsMessage(smsDatabase, messageId);
|
||||
|
||||
if (record.isPresent()) {
|
||||
return Optional.of(new LongMessage(record.get(), ""));
|
||||
return Optional.of(new LongMessage(ConversationMessageFactory.createWithUnresolvedData(context, record.get()), ""));
|
||||
} else {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
|
|
@ -30,6 +30,9 @@ import androidx.lifecycle.ViewModelProviders;
|
|||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
|
@ -42,7 +45,9 @@ import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
|||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
|
||||
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||
|
@ -55,6 +60,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.Function3;
|
||||
import org.thoughtcrime.securesms.util.IOFunction;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
@ -76,6 +82,7 @@ import java.util.HashMap;
|
|||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Encompasses the entire flow of sending media, starting from the selection process to the actual
|
||||
|
@ -130,6 +137,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
private EmojiEditText captionText;
|
||||
private EmojiToggle emojiToggle;
|
||||
private Stub<MediaKeyboard> emojiDrawer;
|
||||
private Stub<View> mentionSuggestions;
|
||||
private TextView charactersLeft;
|
||||
private RecyclerView mediaRail;
|
||||
private MediaRailAdapter mediaRailAdapter;
|
||||
|
@ -142,7 +150,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
/**
|
||||
* Get an intent to launch the media send flow starting with the picker.
|
||||
*/
|
||||
public static Intent buildGalleryIntent(@NonNull Context context, @NonNull Recipient recipient, @Nullable String body, @NonNull TransportOption transport) {
|
||||
public static Intent buildGalleryIntent(@NonNull Context context, @NonNull Recipient recipient, @Nullable CharSequence body, @NonNull TransportOption transport) {
|
||||
Intent intent = new Intent(context, MediaSendActivity.class);
|
||||
intent.putExtra(KEY_RECIPIENT, recipient.getId());
|
||||
intent.putExtra(KEY_TRANSPORT, transport);
|
||||
|
@ -174,7 +182,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
public static Intent buildEditorIntent(@NonNull Context context,
|
||||
@NonNull List<Media> media,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull String body,
|
||||
@NonNull CharSequence body,
|
||||
@NonNull TransportOption transport)
|
||||
{
|
||||
Intent intent = buildGalleryIntent(context, recipient, body, transport);
|
||||
|
@ -207,6 +215,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
charactersLeft = findViewById(R.id.mediasend_characters_left);
|
||||
mediaRail = findViewById(R.id.mediasend_media_rail);
|
||||
emojiDrawer = new Stub<>(findViewById(R.id.mediasend_emoji_drawer_stub));
|
||||
mentionSuggestions = new Stub<>(findViewById(R.id.mediasend_mention_suggestions_stub));
|
||||
|
||||
RecipientId recipientId = getIntent().getParcelableExtra(KEY_RECIPIENT);
|
||||
if (recipientId != null) {
|
||||
|
@ -222,7 +231,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
|
||||
viewModel.setTransport(transport);
|
||||
viewModel.setRecipient(recipient != null ? recipient.get() : null);
|
||||
viewModel.onBodyChanged(getIntent().getStringExtra(KEY_BODY));
|
||||
viewModel.onBodyChanged(getIntent().getCharSequenceExtra(KEY_BODY));
|
||||
|
||||
List<Media> media = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
|
||||
boolean isCamera = getIntent().getBooleanExtra(KEY_IS_CAMERA, false);
|
||||
|
@ -309,6 +318,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
}
|
||||
|
||||
initViewModel();
|
||||
if (FeatureFlags.mentions()) initializeMentionsViewModel();
|
||||
|
||||
revealButton.setOnClickListener(v -> viewModel.onRevealButtonToggled());
|
||||
continueButton.setOnClickListener(v -> navigateToContactSelect());
|
||||
|
@ -525,7 +535,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
MediaSendFragment fragment = getMediaSendFragment();
|
||||
|
||||
if (fragment != null) {
|
||||
viewModel.onSendClicked(buildModelsToTransform(fragment), recipients).observe(this, result -> {
|
||||
viewModel.onSendClicked(buildModelsToTransform(fragment), recipients, composeText.getMentions()).observe(this, result -> {
|
||||
finish();
|
||||
});
|
||||
} else {
|
||||
|
@ -546,7 +556,7 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
|
||||
sendButton.setEnabled(false);
|
||||
|
||||
viewModel.onSendClicked(buildModelsToTransform(fragment), Collections.emptyList()).observe(this, this::setActivityResultAndFinish);
|
||||
viewModel.onSendClicked(buildModelsToTransform(fragment), Collections.emptyList(), composeText.getMentions()).observe(this, this::setActivityResultAndFinish);
|
||||
}
|
||||
|
||||
private static Map<Media, MediaTransform> buildModelsToTransform(@NonNull MediaSendFragment fragment) {
|
||||
|
@ -751,6 +761,46 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
});
|
||||
}
|
||||
|
||||
private void initializeMentionsViewModel() {
|
||||
if (recipient == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
MentionsPickerViewModel mentionsViewModel = ViewModelProviders.of(this, new MentionsPickerViewModel.Factory()).get(MentionsPickerViewModel.class);
|
||||
|
||||
recipient.observe(this, mentionsViewModel::onRecipientChange);
|
||||
composeText.setMentionQueryChangedListener(query -> {
|
||||
if (recipient.get().isPushV2Group()) {
|
||||
if (!mentionSuggestions.resolved()) {
|
||||
mentionSuggestions.get();
|
||||
}
|
||||
mentionsViewModel.onQueryChange(query);
|
||||
}
|
||||
});
|
||||
|
||||
composeText.setMentionValidator(annotations -> {
|
||||
if (!recipient.get().isPushV2Group()) {
|
||||
return annotations;
|
||||
}
|
||||
|
||||
Set<String> validRecipientIds = Stream.of(recipient.get().getParticipants())
|
||||
.map(r -> MentionAnnotation.idToMentionAnnotationValue(r.getId()))
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
return Stream.of(annotations)
|
||||
.filter(a -> !validRecipientIds.contains(a.getValue()))
|
||||
.toList();
|
||||
});
|
||||
|
||||
mentionsViewModel.getSelectedRecipient().observe(this, recipient -> {
|
||||
String replacementDisplayName = recipient.getDisplayName(this);
|
||||
if (replacementDisplayName.equals(recipient.getDisplayUsername())) {
|
||||
replacementDisplayName = recipient.getUsername().or(replacementDisplayName);
|
||||
}
|
||||
composeText.replaceTextWithMention(replacementDisplayName, recipient.getId());
|
||||
});
|
||||
}
|
||||
|
||||
private void presentRecipient(@Nullable Recipient recipient) {
|
||||
if (recipient == null) {
|
||||
composeText.setHint(R.string.MediaSendActivity_message);
|
||||
|
@ -836,9 +886,9 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
|
|||
|
||||
|
||||
private void presentCharactersRemaining() {
|
||||
String messageBody = composeText.getTextTrimmed();
|
||||
String messageBody = composeText.getTextTrimmed().toString();
|
||||
TransportOption transportOption = sendButton.getSelectedTransport();
|
||||
CharacterState characterState = transportOption.calculateCharacters(messageBody);
|
||||
CharacterState characterState = transportOption.calculateCharacters(messageBody);
|
||||
|
||||
if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
|
||||
charactersLeft.setText(String.format(Locale.getDefault(),
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender.PreUploadResult;
|
||||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
|
@ -24,36 +25,41 @@ public class MediaSendActivityResult implements Parcelable {
|
|||
private final String body;
|
||||
private final TransportOption transport;
|
||||
private final boolean viewOnce;
|
||||
private final Collection<Mention> mentions;
|
||||
|
||||
static @NonNull MediaSendActivityResult forPreUpload(@NonNull Collection<PreUploadResult> uploadResults,
|
||||
@NonNull String body,
|
||||
@NonNull TransportOption transport,
|
||||
boolean viewOnce)
|
||||
boolean viewOnce,
|
||||
@NonNull List<Mention> mentions)
|
||||
{
|
||||
Preconditions.checkArgument(uploadResults.size() > 0, "Must supply uploadResults!");
|
||||
return new MediaSendActivityResult(uploadResults, Collections.emptyList(), body, transport, viewOnce);
|
||||
return new MediaSendActivityResult(uploadResults, Collections.emptyList(), body, transport, viewOnce, mentions);
|
||||
}
|
||||
|
||||
static @NonNull MediaSendActivityResult forTraditionalSend(@NonNull List<Media> nonUploadedMedia,
|
||||
@NonNull String body,
|
||||
@NonNull TransportOption transport,
|
||||
boolean viewOnce)
|
||||
boolean viewOnce,
|
||||
@NonNull List<Mention> mentions)
|
||||
{
|
||||
Preconditions.checkArgument(nonUploadedMedia.size() > 0, "Must supply media!");
|
||||
return new MediaSendActivityResult(Collections.emptyList(), nonUploadedMedia, body, transport, viewOnce);
|
||||
return new MediaSendActivityResult(Collections.emptyList(), nonUploadedMedia, body, transport, viewOnce, mentions);
|
||||
}
|
||||
|
||||
private MediaSendActivityResult(@NonNull Collection<PreUploadResult> uploadResults,
|
||||
@NonNull List<Media> nonUploadedMedia,
|
||||
@NonNull String body,
|
||||
@NonNull TransportOption transport,
|
||||
boolean viewOnce)
|
||||
boolean viewOnce,
|
||||
@NonNull List<Mention> mentions)
|
||||
{
|
||||
this.uploadResults = uploadResults;
|
||||
this.nonUploadedMedia = nonUploadedMedia;
|
||||
this.body = body;
|
||||
this.transport = transport;
|
||||
this.viewOnce = viewOnce;
|
||||
this.mentions = mentions;
|
||||
}
|
||||
|
||||
private MediaSendActivityResult(Parcel in) {
|
||||
|
@ -62,6 +68,7 @@ public class MediaSendActivityResult implements Parcelable {
|
|||
this.body = in.readString();
|
||||
this.transport = in.readParcelable(TransportOption.class.getClassLoader());
|
||||
this.viewOnce = ParcelUtil.readBoolean(in);
|
||||
this.mentions = ParcelUtil.readParcelableCollection(in, Mention.class);
|
||||
}
|
||||
|
||||
public boolean isPushPreUpload() {
|
||||
|
@ -88,6 +95,10 @@ public class MediaSendActivityResult implements Parcelable {
|
|||
return viewOnce;
|
||||
}
|
||||
|
||||
public @NonNull Collection<Mention> getMentions() {
|
||||
return mentions;
|
||||
}
|
||||
|
||||
public static final Creator<MediaSendActivityResult> CREATOR = new Creator<MediaSendActivityResult>() {
|
||||
@Override
|
||||
public MediaSendActivityResult createFromParcel(Parcel in) {
|
||||
|
@ -112,5 +123,6 @@ public class MediaSendActivityResult implements Parcelable {
|
|||
dest.writeString(body);
|
||||
dest.writeParcelable(transport, 0);
|
||||
ParcelUtil.writeBoolean(dest, viewOnce);
|
||||
ParcelUtil.writeParcelableCollection(dest, mentions);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import com.annimon.stream.Stream;
|
|||
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
|
@ -453,7 +454,7 @@ class MediaSendViewModel extends ViewModel {
|
|||
savedDrawState.putAll(state);
|
||||
}
|
||||
|
||||
@NonNull LiveData<MediaSendActivityResult> onSendClicked(Map<Media, MediaTransform> modelsToTransform, @NonNull List<Recipient> recipients) {
|
||||
@NonNull LiveData<MediaSendActivityResult> onSendClicked(Map<Media, MediaTransform> modelsToTransform, @NonNull List<Recipient> recipients, @NonNull List<Mention> mentions) {
|
||||
if (isSms && recipients.size() > 0) {
|
||||
throw new IllegalStateException("Provided recipients to send to, but this is SMS!");
|
||||
}
|
||||
|
@ -476,7 +477,7 @@ class MediaSendViewModel extends ViewModel {
|
|||
|
||||
if (isSms || MessageSender.isLocalSelfSend(application, recipient, isSms)) {
|
||||
Log.i(TAG, "SMS or local self-send. Skipping pre-upload.");
|
||||
result.postValue(MediaSendActivityResult.forTraditionalSend(updatedMedia, trimmedBody, transport, isViewOnce()));
|
||||
result.postValue(MediaSendActivityResult.forTraditionalSend(updatedMedia, trimmedBody, transport, isViewOnce(), mentions));
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -493,12 +494,12 @@ class MediaSendViewModel extends ViewModel {
|
|||
uploadRepository.updateDisplayOrder(updatedMedia);
|
||||
uploadRepository.getPreUploadResults(uploadResults -> {
|
||||
if (recipients.size() > 0) {
|
||||
sendMessages(recipients, splitBody, uploadResults);
|
||||
sendMessages(recipients, splitBody, uploadResults, mentions);
|
||||
uploadRepository.deleteAbandonedAttachments();
|
||||
}
|
||||
|
||||
Util.cancelRunnableOnMain(dialogRunnable);
|
||||
result.postValue(MediaSendActivityResult.forPreUpload(uploadResults, splitBody, transport, isViewOnce()));
|
||||
result.postValue(MediaSendActivityResult.forPreUpload(uploadResults, splitBody, transport, isViewOnce(), mentions));
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -632,7 +633,7 @@ class MediaSendViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
@WorkerThread
|
||||
private void sendMessages(@NonNull List<Recipient> recipients, @NonNull String body, @NonNull Collection<PreUploadResult> preUploadResults) {
|
||||
private void sendMessages(@NonNull List<Recipient> recipients, @NonNull String body, @NonNull Collection<PreUploadResult> preUploadResults, @NonNull List<Mention> mentions) {
|
||||
List<OutgoingSecureMediaMessage> messages = new ArrayList<>(recipients.size());
|
||||
|
||||
for (Recipient recipient : recipients) {
|
||||
|
@ -647,6 +648,7 @@ class MediaSendViewModel extends ViewModel {
|
|||
null,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
mentions,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import com.annimon.stream.ComparatorCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
|
||||
|
@ -17,7 +18,7 @@ final class MessageDetails {
|
|||
private static final Comparator<RecipientDeliveryStatus> ALPHABETICAL = (r1, r2) -> r1.getRecipient().getDisplayName(ApplicationDependencies.getApplication()).compareToIgnoreCase(r2.getRecipient().getDisplayName(ApplicationDependencies.getApplication()));
|
||||
private static final Comparator<RecipientDeliveryStatus> RECIPIENT_COMPARATOR = ComparatorCompat.chain(HAS_DISPLAY_NAME).thenComparing(ALPHABETICAL);
|
||||
|
||||
private final MessageRecord messageRecord;
|
||||
private final ConversationMessage conversationMessage;
|
||||
|
||||
private final Collection<RecipientDeliveryStatus> pending;
|
||||
private final Collection<RecipientDeliveryStatus> sent;
|
||||
|
@ -25,8 +26,8 @@ final class MessageDetails {
|
|||
private final Collection<RecipientDeliveryStatus> read;
|
||||
private final Collection<RecipientDeliveryStatus> notSent;
|
||||
|
||||
MessageDetails(MessageRecord messageRecord, List<RecipientDeliveryStatus> recipients) {
|
||||
this.messageRecord = messageRecord;
|
||||
MessageDetails(@NonNull ConversationMessage conversationMessage, @NonNull List<RecipientDeliveryStatus> recipients) {
|
||||
this.conversationMessage = conversationMessage;
|
||||
|
||||
pending = new TreeSet<>(RECIPIENT_COMPARATOR);
|
||||
sent = new TreeSet<>(RECIPIENT_COMPARATOR);
|
||||
|
@ -34,7 +35,7 @@ final class MessageDetails {
|
|||
read = new TreeSet<>(RECIPIENT_COMPARATOR);
|
||||
notSent = new TreeSet<>(RECIPIENT_COMPARATOR);
|
||||
|
||||
if (messageRecord.isOutgoing()) {
|
||||
if (conversationMessage.getMessageRecord().isOutgoing()) {
|
||||
for (RecipientDeliveryStatus status : recipients) {
|
||||
switch (status.getDeliveryStatus()) {
|
||||
case UNKNOWN:
|
||||
|
@ -59,8 +60,8 @@ final class MessageDetails {
|
|||
}
|
||||
}
|
||||
|
||||
@NonNull MessageRecord getMessageRecord() {
|
||||
return messageRecord;
|
||||
@NonNull ConversationMessage getConversationMessage() {
|
||||
return conversationMessage;
|
||||
}
|
||||
|
||||
@NonNull Collection<RecipientDeliveryStatus> getPending() {
|
||||
|
|
|
@ -124,7 +124,7 @@ public final class MessageDetailsActivity extends PassphraseRequiredActivity {
|
|||
assert getSupportActionBar() != null;
|
||||
getSupportActionBar().setBackgroundDrawable(new ColorDrawable(color.toActionBarColor(this)));
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
getWindow().setStatusBarColor(color.toStatusBarColor(this));
|
||||
}
|
||||
}
|
||||
|
@ -132,9 +132,9 @@ public final class MessageDetailsActivity extends PassphraseRequiredActivity {
|
|||
private List<MessageDetailsViewState<?>> convertToRows(MessageDetails details) {
|
||||
List<MessageDetailsViewState<?>> list = new ArrayList<>();
|
||||
|
||||
list.add(new MessageDetailsViewState<>(details.getMessageRecord(), MessageDetailsViewState.MESSAGE_HEADER));
|
||||
list.add(new MessageDetailsViewState<>(details.getConversationMessage(), MessageDetailsViewState.MESSAGE_HEADER));
|
||||
|
||||
if (details.getMessageRecord().isOutgoing()) {
|
||||
if (details.getConversationMessage().getMessageRecord().isOutgoing()) {
|
||||
addRecipients(list, RecipientHeader.NOT_SENT, details.getNotSent());
|
||||
addRecipients(list, RecipientHeader.READ, details.getRead());
|
||||
addRecipients(list, RecipientHeader.DELIVERED, details.getDelivered());
|
||||
|
|
|
@ -10,6 +10,7 @@ import androidx.recyclerview.widget.ListAdapter;
|
|||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
|
@ -45,7 +46,7 @@ final class MessageDetailsAdapter extends ListAdapter<MessageDetailsAdapter.Mess
|
|||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
if (holder instanceof MessageHeaderViewHolder) {
|
||||
((MessageHeaderViewHolder) holder).bind((MessageRecord) getItem(position).data, running);
|
||||
((MessageHeaderViewHolder) holder).bind((ConversationMessage) getItem(position).data, running);
|
||||
} else if (holder instanceof RecipientHeaderViewHolder) {
|
||||
((RecipientHeaderViewHolder) holder).bind((RecipientHeader) getItem(position).data);
|
||||
} else if (holder instanceof RecipientViewHolder) {
|
||||
|
@ -60,7 +61,7 @@ final class MessageDetailsAdapter extends ListAdapter<MessageDetailsAdapter.Mess
|
|||
if (payloads.isEmpty()) {
|
||||
super.onBindViewHolder(holder, position, payloads);
|
||||
} else if (holder instanceof MessageHeaderViewHolder) {
|
||||
((MessageHeaderViewHolder) holder).partialBind((MessageRecord) getItem(position).data, running);
|
||||
((MessageHeaderViewHolder) holder).partialBind((ConversationMessage) getItem(position).data, running);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,11 +8,14 @@ import androidx.annotation.WorkerThread;
|
|||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -86,7 +89,7 @@ final class MessageDetailsRepository {
|
|||
}
|
||||
}
|
||||
|
||||
return new MessageDetails(messageRecord, recipients);
|
||||
return new MessageDetails(ConversationMessageFactory.createWithUnresolvedData(context, messageRecord), recipients);
|
||||
}
|
||||
|
||||
private @Nullable NetworkFailure getNetworkFailure(MessageRecord messageRecord, Recipient recipient) {
|
||||
|
|
|
@ -13,6 +13,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItem;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
|
@ -63,25 +64,30 @@ final class MessageHeaderViewHolder extends RecyclerView.ViewHolder {
|
|||
receivedStub = itemView.findViewById(R.id.message_details_header_message_view_received_multimedia);
|
||||
}
|
||||
|
||||
void bind(MessageRecord messageRecord, boolean running) {
|
||||
bindMessageView(messageRecord);
|
||||
void bind(ConversationMessage conversationMessage, boolean running) {
|
||||
MessageRecord messageRecord = conversationMessage.getMessageRecord();
|
||||
bindMessageView(conversationMessage);
|
||||
bindErrorState(messageRecord);
|
||||
bindSentReceivedDates(messageRecord);
|
||||
bindExpirationTime(messageRecord, running);
|
||||
bindTransport(messageRecord);
|
||||
}
|
||||
|
||||
void partialBind(MessageRecord messageRecord, boolean running) {
|
||||
bindExpirationTime(messageRecord, running);
|
||||
void partialBind(ConversationMessage conversationMessage, boolean running) {
|
||||
bindExpirationTime(conversationMessage.getMessageRecord(), running);
|
||||
}
|
||||
|
||||
private void bindMessageView(MessageRecord messageRecord) {
|
||||
private void bindMessageView(ConversationMessage conversationMessage) {
|
||||
if (conversationItem == null) {
|
||||
if (messageRecord.isGroupAction()) conversationItem = (ConversationItem) updateStub.inflate();
|
||||
else if (messageRecord.isOutgoing()) conversationItem = (ConversationItem) sentStub.inflate();
|
||||
else conversationItem = (ConversationItem) receivedStub.inflate();
|
||||
if (conversationMessage.getMessageRecord().isGroupAction()) {
|
||||
conversationItem = (ConversationItem) updateStub.inflate();
|
||||
} else if (conversationMessage.getMessageRecord().isOutgoing()) {
|
||||
conversationItem = (ConversationItem) sentStub.inflate();
|
||||
} else {
|
||||
conversationItem = (ConversationItem) receivedStub.inflate();
|
||||
}
|
||||
}
|
||||
conversationItem.bind(new ConversationMessage(messageRecord), Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), messageRecord.getRecipient(), null, false);
|
||||
conversationItem.bind(conversationMessage, Optional.absent(), Optional.absent(), glideRequests, Locale.getDefault(), new HashSet<>(), conversationMessage.getMessageRecord().getRecipient(), null, false);
|
||||
}
|
||||
|
||||
private void bindErrorState(MessageRecord messageRecord) {
|
||||
|
|
|
@ -377,7 +377,7 @@ public class AttachmentManager {
|
|||
.execute();
|
||||
}
|
||||
|
||||
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull String body, @NonNull TransportOption transport) {
|
||||
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull CharSequence body, @NonNull TransportOption transport) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.annotation.NonNull;
|
|||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
@ -35,6 +36,7 @@ public class IncomingMediaMessage {
|
|||
private final List<Attachment> attachments = new LinkedList<>();
|
||||
private final List<Contact> sharedContacts = new LinkedList<>();
|
||||
private final List<LinkPreview> linkPreviews = new LinkedList<>();
|
||||
private final List<Mention> mentions = new LinkedList<>();
|
||||
|
||||
public IncomingMediaMessage(@NonNull RecipientId from,
|
||||
Optional<GroupId> groupId,
|
||||
|
@ -78,6 +80,7 @@ public class IncomingMediaMessage {
|
|||
Optional<QuoteModel> quote,
|
||||
Optional<List<Contact>> sharedContacts,
|
||||
Optional<List<LinkPreview>> linkPreviews,
|
||||
Optional<List<Mention>> mentions,
|
||||
Optional<Attachment> sticker)
|
||||
{
|
||||
this.push = true;
|
||||
|
@ -98,6 +101,7 @@ public class IncomingMediaMessage {
|
|||
this.attachments.addAll(PointerAttachment.forPointers(attachments));
|
||||
this.sharedContacts.addAll(sharedContacts.or(Collections.emptyList()));
|
||||
this.linkPreviews.addAll(linkPreviews.or(Collections.emptyList()));
|
||||
this.mentions.addAll(mentions.or(Collections.emptyList()));
|
||||
|
||||
if (sticker.isPresent()) {
|
||||
this.attachments.add(sticker.get());
|
||||
|
@ -164,6 +168,10 @@ public class IncomingMediaMessage {
|
|||
return linkPreviews;
|
||||
}
|
||||
|
||||
public @NonNull List<Mention> getMentions() {
|
||||
return mentions;
|
||||
}
|
||||
|
||||
public boolean isUnidentified() {
|
||||
return unidentified;
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ public class OutgoingExpirationUpdateMessage extends OutgoingSecureMediaMessage
|
|||
public OutgoingExpirationUpdateMessage(Recipient recipient, long sentTimeMillis, long expiresIn) {
|
||||
super(recipient, "", new LinkedList<Attachment>(), sentTimeMillis,
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, false, null, Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.annotation.Nullable;
|
|||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -26,10 +27,11 @@ public final class OutgoingGroupUpdateMessage extends OutgoingSecureMediaMessage
|
|||
boolean viewOnce,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
@NonNull List<LinkPreview> previews,
|
||||
@NonNull List<Mention> mentions)
|
||||
{
|
||||
super(recipient, groupContext.getEncodedGroupContext(), avatar, sentTimeMillis,
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, viewOnce, quote, contacts, previews);
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION, expiresIn, viewOnce, quote, contacts, previews, mentions);
|
||||
|
||||
this.messageGroupContext = groupContext;
|
||||
}
|
||||
|
@ -42,9 +44,10 @@ public final class OutgoingGroupUpdateMessage extends OutgoingSecureMediaMessage
|
|||
boolean viewOnce,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
@NonNull List<LinkPreview> previews,
|
||||
@NonNull List<Mention> mentions)
|
||||
{
|
||||
this(recipient, new MessageGroupContext(group), getAttachments(avatar), sentTimeMillis, expireIn, viewOnce, quote, contacts, previews);
|
||||
this(recipient, new MessageGroupContext(group), getAttachments(avatar), sentTimeMillis, expireIn, viewOnce, quote, contacts, previews, mentions);
|
||||
}
|
||||
|
||||
public OutgoingGroupUpdateMessage(@NonNull Recipient recipient,
|
||||
|
@ -55,9 +58,10 @@ public final class OutgoingGroupUpdateMessage extends OutgoingSecureMediaMessage
|
|||
boolean viewOnce,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
@NonNull List<LinkPreview> previews,
|
||||
@NonNull List<Mention> mentions)
|
||||
{
|
||||
this(recipient, new MessageGroupContext(group), getAttachments(avatar), sentTimeMillis, expireIn, viewOnce, quote, contacts, previews);
|
||||
this(recipient, new MessageGroupContext(group), getAttachments(avatar), sentTimeMillis, expireIn, viewOnce, quote, contacts, previews, mentions);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.attachments.Attachment;
|
|||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
|
@ -30,6 +31,7 @@ public class OutgoingMediaMessage {
|
|||
private final List<IdentityKeyMismatch> identityKeyMismatches = new LinkedList<>();
|
||||
private final List<Contact> contacts = new LinkedList<>();
|
||||
private final List<LinkPreview> linkPreviews = new LinkedList<>();
|
||||
private final List<Mention> mentions = new LinkedList<>();
|
||||
|
||||
public OutgoingMediaMessage(Recipient recipient, String message,
|
||||
List<Attachment> attachments, long sentTimeMillis,
|
||||
|
@ -38,6 +40,7 @@ public class OutgoingMediaMessage {
|
|||
@Nullable QuoteModel outgoingQuote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews,
|
||||
@NonNull List<Mention> mentions,
|
||||
@NonNull List<NetworkFailure> networkFailures,
|
||||
@NonNull List<IdentityKeyMismatch> identityKeyMismatches)
|
||||
{
|
||||
|
@ -53,6 +56,7 @@ public class OutgoingMediaMessage {
|
|||
|
||||
this.contacts.addAll(contacts);
|
||||
this.linkPreviews.addAll(linkPreviews);
|
||||
this.mentions.addAll(mentions);
|
||||
this.networkFailures.addAll(networkFailures);
|
||||
this.identityKeyMismatches.addAll(identityKeyMismatches);
|
||||
}
|
||||
|
@ -62,14 +66,15 @@ public class OutgoingMediaMessage {
|
|||
boolean viewOnce, int distributionType,
|
||||
@Nullable QuoteModel outgoingQuote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews)
|
||||
@NonNull List<LinkPreview> linkPreviews,
|
||||
@NonNull List<Mention> mentions)
|
||||
{
|
||||
this(recipient,
|
||||
buildMessage(slideDeck, message),
|
||||
slideDeck.asAttachments(),
|
||||
sentTimeMillis, subscriptionId,
|
||||
expiresIn, viewOnce, distributionType, outgoingQuote,
|
||||
contacts, linkPreviews, new LinkedList<>(), new LinkedList<>());
|
||||
contacts, linkPreviews, mentions, new LinkedList<>(), new LinkedList<>());
|
||||
}
|
||||
|
||||
public OutgoingMediaMessage(OutgoingMediaMessage that) {
|
||||
|
@ -87,6 +92,7 @@ public class OutgoingMediaMessage {
|
|||
this.networkFailures.addAll(that.networkFailures);
|
||||
this.contacts.addAll(that.contacts);
|
||||
this.linkPreviews.addAll(that.linkPreviews);
|
||||
this.mentions.addAll(that.mentions);
|
||||
}
|
||||
|
||||
public Recipient getRecipient() {
|
||||
|
@ -145,6 +151,10 @@ public class OutgoingMediaMessage {
|
|||
return linkPreviews;
|
||||
}
|
||||
|
||||
public @NonNull List<Mention> getMentions() {
|
||||
return mentions;
|
||||
}
|
||||
|
||||
public @NonNull List<NetworkFailure> getNetworkFailures() {
|
||||
return networkFailures;
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
|
@ -21,9 +22,10 @@ public class OutgoingSecureMediaMessage extends OutgoingMediaMessage {
|
|||
boolean viewOnce,
|
||||
@Nullable QuoteModel quote,
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> previews)
|
||||
@NonNull List<LinkPreview> previews,
|
||||
@NonNull List<Mention> mentions)
|
||||
{
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, viewOnce, distributionType, quote, contacts, previews, Collections.emptyList(), Collections.emptyList());
|
||||
super(recipient, body, attachments, sentTimeMillis, -1, expiresIn, viewOnce, distributionType, quote, contacts, previews, mentions, Collections.emptyList(), Collections.emptyList());
|
||||
}
|
||||
|
||||
public OutgoingSecureMediaMessage(OutgoingMediaMessage base) {
|
||||
|
|
|
@ -5,8 +5,10 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public class QuoteModel {
|
||||
|
@ -16,13 +18,15 @@ public class QuoteModel {
|
|||
private final String text;
|
||||
private final boolean missing;
|
||||
private final List<Attachment> attachments;
|
||||
private final List<Mention> mentions;
|
||||
|
||||
public QuoteModel(long id, @NonNull RecipientId author, String text, boolean missing, @Nullable List<Attachment> attachments) {
|
||||
public QuoteModel(long id, @NonNull RecipientId author, String text, boolean missing, @Nullable List<Attachment> attachments, @Nullable List<Mention> mentions) {
|
||||
this.id = id;
|
||||
this.author = author;
|
||||
this.text = text;
|
||||
this.missing = missing;
|
||||
this.attachments = attachments;
|
||||
this.mentions = mentions != null ? mentions : Collections.emptyList();
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
|
@ -44,4 +48,8 @@ public class QuoteModel {
|
|||
public List<Attachment> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public @NonNull List<Mention> getMentions() {
|
||||
return mentions;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,7 +75,20 @@ public class AndroidAutoReplyReceiver extends BroadcastReceiver {
|
|||
|
||||
if (recipient.resolve().isGroup()) {
|
||||
Log.w(TAG, "GroupRecipient, Sending media message");
|
||||
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, false, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient,
|
||||
responseText.toString(),
|
||||
new LinkedList<>(),
|
||||
System.currentTimeMillis(),
|
||||
subscriptionId,
|
||||
expiresIn,
|
||||
false,
|
||||
0,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
replyThreadId = MessageSender.send(context, reply, threadId, false, null);
|
||||
} else {
|
||||
Log.w(TAG, "Sending regular message ");
|
||||
|
|
|
@ -43,15 +43,18 @@ import org.thoughtcrime.securesms.contactshare.Contact;
|
|||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationActivity;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadBodyUtil;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
|
@ -59,6 +62,7 @@ import org.thoughtcrime.securesms.mms.SlideDeck;
|
|||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
|
@ -243,18 +247,14 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||
{
|
||||
boolean isVisible = visibleThread == threadId;
|
||||
|
||||
ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context);
|
||||
Recipient recipients = DatabaseFactory.getThreadDatabase(context)
|
||||
.getRecipientForThreadId(threadId);
|
||||
ThreadDatabase threads = DatabaseFactory.getThreadDatabase(context);
|
||||
|
||||
if (isVisible) {
|
||||
List<MarkedMessageInfo> messageIds = threads.setRead(threadId, false);
|
||||
MarkReadReceiver.process(context, messageIds);
|
||||
}
|
||||
|
||||
if (!TextSecurePreferences.isNotificationsEnabled(context) ||
|
||||
(recipients != null && recipients.isMuted()))
|
||||
{
|
||||
if (!TextSecurePreferences.isNotificationsEnabled(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -499,7 +499,7 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||
Recipient recipient = record.getIndividualRecipient().resolve();
|
||||
Recipient conversationRecipient = record.getRecipient().resolve();
|
||||
long threadId = record.getThreadId();
|
||||
CharSequence body = record.getDisplayBody(context);
|
||||
CharSequence body = MentionUtil.updateBodyWithDisplayNames(context, record);
|
||||
Recipient threadRecipients = null;
|
||||
SlideDeck slideDeck = null;
|
||||
long timestamp = record.getTimestamp();
|
||||
|
@ -527,7 +527,17 @@ public class DefaultMessageNotifier implements MessageNotifier {
|
|||
slideDeck = ((MmsMessageRecord) record).getSlideDeck();
|
||||
}
|
||||
|
||||
if (threadRecipients == null || !threadRecipients.isMuted()) {
|
||||
boolean includeMessage = true;
|
||||
if (threadRecipients != null && threadRecipients.isMuted()) {
|
||||
RecipientDatabase.MentionSetting mentionSetting = threadRecipients.getMentionSetting();
|
||||
|
||||
boolean overrideMuted = (mentionSetting == RecipientDatabase.MentionSetting.GLOBAL && SignalStore.notificationSettings().isMentionNotifiesMeEnabled()) ||
|
||||
mentionSetting == RecipientDatabase.MentionSetting.ALWAYS_NOTIFY;
|
||||
|
||||
includeMessage = FeatureFlags.mentions() && overrideMuted && record.hasSelfMention();
|
||||
}
|
||||
|
||||
if (threadRecipients == null || includeMessage) {
|
||||
notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, receivedTimestamp, slideDeck, false));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,7 +77,20 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
|||
|
||||
switch (replyMethod) {
|
||||
case GroupMessage: {
|
||||
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient, responseText.toString(), new LinkedList<>(), System.currentTimeMillis(), subscriptionId, expiresIn, false, 0, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
|
||||
OutgoingMediaMessage reply = new OutgoingMediaMessage(recipient,
|
||||
responseText.toString(),
|
||||
new LinkedList<>(),
|
||||
System.currentTimeMillis(),
|
||||
subscriptionId,
|
||||
expiresIn,
|
||||
false,
|
||||
0,
|
||||
null,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
threadId = MessageSender.send(context, reply, -1, false, null);
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -12,13 +12,18 @@ import android.provider.Settings;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceDataStore;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.keyvalue.NotificationSettings;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
@ -115,11 +120,18 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
|
|||
initializeCallRingtoneSummary(findPreference(TextSecurePreferences.CALL_RINGTONE_PREF));
|
||||
initializeMessageVibrateSummary((SwitchPreferenceCompat)findPreference(TextSecurePreferences.VIBRATE_PREF));
|
||||
initializeCallVibrateSummary((SwitchPreferenceCompat)findPreference(TextSecurePreferences.CALL_VIBRATE_PREF));
|
||||
|
||||
if (FeatureFlags.mentions()) {
|
||||
initializeMentionsNotifyMeSummary((SwitchPreferenceCompat)findPreference(NotificationSettings.MENTIONS_NOTIFY_ME));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences_notifications);
|
||||
if (FeatureFlags.mentions()) {
|
||||
addPreferencesFromResource(R.xml.preferences_notifications_mentions);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -197,6 +209,11 @@ public class NotificationsPreferenceFragment extends ListSummaryPreferenceFragme
|
|||
pref.setChecked(TextSecurePreferences.isCallNotificationVibrateEnabled(getContext()));
|
||||
}
|
||||
|
||||
private void initializeMentionsNotifyMeSummary(SwitchPreferenceCompat pref) {
|
||||
pref.setPreferenceDataStore(SignalStore.getPreferenceDataStore());
|
||||
pref.setChecked(SignalStore.notificationSettings().isMentionNotifiesMeEnabled());
|
||||
}
|
||||
|
||||
public static CharSequence getSummary(Context context) {
|
||||
final int onCapsResId = R.string.ApplicationPreferencesActivity_On;
|
||||
final int offCapsResId = R.string.ApplicationPreferencesActivity_Off;
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.contacts.avatars.TransparentContactPhoto;
|
|||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.MentionSetting;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
|
||||
|
@ -102,6 +103,7 @@ public class Recipient {
|
|||
private final byte[] storageId;
|
||||
private final byte[] identityKey;
|
||||
private final VerifiedStatus identityStatus;
|
||||
private final MentionSetting mentionSetting;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -316,6 +318,7 @@ public class Recipient {
|
|||
this.storageId = null;
|
||||
this.identityKey = null;
|
||||
this.identityStatus = VerifiedStatus.DEFAULT;
|
||||
this.mentionSetting = MentionSetting.GLOBAL;
|
||||
}
|
||||
|
||||
public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) {
|
||||
|
@ -359,6 +362,7 @@ public class Recipient {
|
|||
this.storageId = details.storageId;
|
||||
this.identityKey = details.identityKey;
|
||||
this.identityStatus = details.identityStatus;
|
||||
this.mentionSetting = details.mentionSetting;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getId() {
|
||||
|
@ -809,6 +813,10 @@ public class Recipient {
|
|||
}
|
||||
}
|
||||
|
||||
public @NonNull MentionSetting getMentionSetting() {
|
||||
return mentionSetting;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
|
|
@ -9,7 +9,9 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.VerifiedStatus;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.MentionSetting;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
||||
|
@ -66,6 +68,7 @@ public class RecipientDetails {
|
|||
final byte[] storageId;
|
||||
final byte[] identityKey;
|
||||
final VerifiedStatus identityStatus;
|
||||
final MentionSetting mentionSetting;
|
||||
|
||||
public RecipientDetails(@Nullable String name,
|
||||
@NonNull Optional<Long> groupAvatarId,
|
||||
|
@ -112,6 +115,7 @@ public class RecipientDetails {
|
|||
this.storageId = settings.getStorageId();
|
||||
this.identityKey = settings.getIdentityKey();
|
||||
this.identityStatus = settings.getIdentityStatus();
|
||||
this.mentionSetting = settings.getMentionSetting();
|
||||
|
||||
if (name == null) this.name = settings.getSystemDisplayName();
|
||||
else this.name = name;
|
||||
|
@ -160,6 +164,7 @@ public class RecipientDetails {
|
|||
this.storageId = null;
|
||||
this.identityKey = null;
|
||||
this.identityStatus = VerifiedStatus.DEFAULT;
|
||||
this.mentionSetting = MentionSetting.GLOBAL;
|
||||
}
|
||||
|
||||
public static @NonNull RecipientDetails forIndividual(@NonNull Context context, @NonNull RecipientSettings settings) {
|
||||
|
|
|
@ -296,7 +296,7 @@ public class ShareActivity extends PassphraseRequiredActivity
|
|||
|
||||
private void openConversation(long threadId, @NonNull RecipientId recipientId, @Nullable ShareData shareData) {
|
||||
Intent intent = new Intent(this, ConversationActivity.class);
|
||||
String textExtra = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
CharSequence textExtra = getIntent().getCharSequenceExtra(Intent.EXTRA_TEXT);
|
||||
ArrayList<Media> mediaExtra = getIntent().getParcelableArrayListExtra(ConversationActivity.MEDIA_EXTRA);
|
||||
StickerLocator stickerExtra = getIntent().getParcelableExtra(ConversationActivity.STICKER_EXTRA);
|
||||
boolean borderlessExtra = getIntent().getBooleanExtra(ConversationActivity.BORDERLESS_EXTRA, false);
|
||||
|
|
|
@ -180,4 +180,21 @@ public final class StringUtil {
|
|||
.appendCodePoint(Bidi.PDI)
|
||||
.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Trims a {@link CharSequence} of starting and trailing whitespace. Behavior matches
|
||||
* {@link String#trim()} to preserve expectations around results.
|
||||
*/
|
||||
public static CharSequence trimSequence(CharSequence text) {
|
||||
int length = text.length();
|
||||
int startIndex = 0;
|
||||
|
||||
while ((startIndex < length) && (text.charAt(startIndex) <= ' ')) {
|
||||
startIndex++;
|
||||
}
|
||||
while ((startIndex < length) && (text.charAt(length - 1) <= ' ')) {
|
||||
length--;
|
||||
}
|
||||
return (startIndex > 0 || length < text.length()) ? text.subSequence(startIndex, length) : text;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -176,6 +176,10 @@ public class Util {
|
|||
return value != null ? value : "";
|
||||
}
|
||||
|
||||
public static @NonNull CharSequence emptyIfNull(@Nullable CharSequence value) {
|
||||
return value != null ? value : "";
|
||||
}
|
||||
|
||||
public static <E> List<List<E>> chunk(@NonNull List<E> list, int chunkSize) {
|
||||
List<List<E>> chunks = new ArrayList<>(list.size() / chunkSize);
|
||||
|
||||
|
|
|
@ -55,3 +55,16 @@ message ProfileChangeDetails {
|
|||
|
||||
StringChange profileNameChange = 1;
|
||||
}
|
||||
|
||||
message BodyRangeList {
|
||||
message BodyRange {
|
||||
int32 start = 1;
|
||||
int32 length = 2;
|
||||
|
||||
oneof associatedValue {
|
||||
string mentionUuid = 3;
|
||||
}
|
||||
}
|
||||
|
||||
repeated BodyRange ranges = 1;
|
||||
}
|
||||
|
|
|
@ -290,6 +290,46 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/group_mentions_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/group_manage_fragment_row_height"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/group_mentions"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:gravity="start|center_vertical"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:text="@string/ManageGroupActivity_mentions"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/group_mute_notifications_switch"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/group_mentions_value"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical|end"
|
||||
android:paddingStart="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:paddingEnd="@dimen/group_manage_fragment_row_horizontal_padding"
|
||||
android:textAppearance="@style/Signal.Text.Body"
|
||||
android:textColor="@color/ultramarine_text_button"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/group_custom_notifications"
|
||||
app:layout_constraintTop_toBottomOf="@id/group_mute_notifications_switch"
|
||||
tools:text="Default (Notify me)" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
<?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="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
style="@style/TextAppearance.AppCompat.Subhead"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:paddingLeft="?attr/dialogPreferredPadding"
|
||||
android:paddingRight="?attr/dialogPreferredPadding"
|
||||
android:text="@string/GroupMentionSettingDialog_receive_notifications_when_youre_mentioned_in_muted_chats" />
|
||||
|
||||
<CheckedTextView
|
||||
android:id="@+id/group_mention_setting_default"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawableStart="?android:attr/listChoiceIndicatorSingle"
|
||||
android:drawablePadding="20dp"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?attr/listPreferredItemHeightSmall"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="?attr/dialogPreferredPadding"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?attr/textColorAlertDialogListItem"
|
||||
tools:text="@string/GroupMentionSettingDialog_default_notify_me" />
|
||||
|
||||
<CheckedTextView
|
||||
android:id="@+id/group_mention_setting_always_notify"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawableStart="?android:attr/listChoiceIndicatorSingle"
|
||||
android:drawablePadding="20dp"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?attr/listPreferredItemHeightSmall"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="?attr/dialogPreferredPadding"
|
||||
android:text="@string/GroupMentionSettingDialog_always_notify_me"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?attr/textColorAlertDialogListItem" />
|
||||
|
||||
<CheckedTextView
|
||||
android:id="@+id/group_mention_setting_dont_notify"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:drawableStart="?android:attr/listChoiceIndicatorSingle"
|
||||
android:drawablePadding="20dp"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?attr/listPreferredItemHeightSmall"
|
||||
android:paddingStart="20dp"
|
||||
android:paddingEnd="?attr/dialogPreferredPadding"
|
||||
android:text="@string/GroupMentionSettingDialog_dont_notify_me"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="?attr/textColorAlertDialogListItem" />
|
||||
|
||||
</LinearLayout>
|
|
@ -27,6 +27,13 @@
|
|||
android:clickable="true"
|
||||
android:background="@color/transparent_black_40">
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/mediasend_mention_suggestions_stub"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout="@layout/conversation_mention_suggestions_stub"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiEditText
|
||||
android:id="@+id/mediasend_caption"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -117,7 +124,7 @@
|
|||
android:layout_marginStart="12dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/circle_tintable"
|
||||
tools:backgroundTint="@color/core_blue">
|
||||
tools:backgroundTint="@color/core_ultramarine">
|
||||
|
||||
<org.thoughtcrime.securesms.components.SendButton
|
||||
android:id="@+id/mediasend_send_button"
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/core_grey_65"/>
|
||||
android:background="?conversation_mention_divider_color"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/mentions_picker_list"
|
||||
|
|
|
@ -3,11 +3,12 @@
|
|||
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:background="?selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:background="?selectableItemBackground">
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="10dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/mention_recipient_avatar"
|
||||
|
@ -17,12 +18,25 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/mention_recipient_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Preview"
|
||||
tools:text="@tools:sample/full_names" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mention_recipient_username"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="viewStart"
|
||||
android:textAppearance="@style/Signal.Text.Preview"
|
||||
android:textColor="@color/core_grey_60"
|
||||
tools:text="@tools:sample/last_names" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -98,6 +98,7 @@
|
|||
style="@style/Signal.Text.Body"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
app:emoji_renderMentions="false"
|
||||
tools:text="With great power comes great responsibility."
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
|
|
@ -90,6 +90,7 @@
|
|||
<attr name="conversation_title_color" format="reference" />
|
||||
<attr name="conversation_subtitle_color" format="reference" />
|
||||
<attr name="conversation_mention_background_color" format="reference" />
|
||||
<attr name="conversation_mention_divider_color" format="reference" />
|
||||
|
||||
<attr name="emoji_tab_strip_background" format="color" />
|
||||
<attr name="emoji_tab_indicator" format="color" />
|
||||
|
@ -404,6 +405,7 @@
|
|||
<attr name="scaleEmojis" format="boolean" />
|
||||
<attr name="emoji_maxLength" format="integer" />
|
||||
<attr name="emoji_forceCustom" format="boolean" />
|
||||
<attr name="emoji_renderMentions" format="boolean" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="RingtonePreference">
|
||||
|
|
|
@ -159,4 +159,6 @@
|
|||
<dimen name="group_manage_fragment_row_horizontal_padding">16dp</dimen>
|
||||
|
||||
<dimen name="wave_form_bar_width">2dp</dimen>
|
||||
|
||||
<dimen name="mentions_picker_peek_height">216dp</dimen>
|
||||
</resources>
|
||||
|
|
|
@ -549,6 +549,7 @@
|
|||
<string name="ManageGroupActivity_leave_group">Leave group</string>
|
||||
<string name="ManageGroupActivity_mute_notifications">Mute notifications</string>
|
||||
<string name="ManageGroupActivity_custom_notifications">Custom notifications</string>
|
||||
<string name="ManageGroupActivity_mentions">Mentions</string>
|
||||
<string name="ManageGroupActivity_until_s">Until %1$s</string>
|
||||
<string name="ManageGroupActivity_off">Off</string>
|
||||
<string name="ManageGroupActivity_on">On</string>
|
||||
|
@ -576,6 +577,13 @@
|
|||
<string name="ManageGroupActivity_legacy_group">Legacy Group</string>
|
||||
<string name="ManageGroupActivity_legacy_group_learn_more">This is a Legacy Group. To access features like group admins, create a New Group.</string>
|
||||
|
||||
<!-- GroupMentionSettingDialog -->
|
||||
<string name="GroupMentionSettingDialog_notify_me_for_mentions">Notify me for Mentions</string>
|
||||
<string name="GroupMentionSettingDialog_receive_notifications_when_youre_mentioned_in_muted_chats">Receive notifications when you’re mentioned in muted chats?</string>
|
||||
<string name="GroupMentionSettingDialog_default_notify_me">Default (Notify me)</string>
|
||||
<string name="GroupMentionSettingDialog_default_dont_notify_me">Default (Don\'t notify me)</string>
|
||||
<string name="GroupMentionSettingDialog_always_notify_me">Always notify me</string>
|
||||
<string name="GroupMentionSettingDialog_dont_notify_me">Don\'t notify me</string>
|
||||
|
||||
<!-- ManageRecipientActivity -->
|
||||
<string name="ManageRecipientActivity_add_to_system_contacts">Add to system contacts</string>
|
||||
|
@ -1995,6 +2003,9 @@
|
|||
<string name="preferences_communication__sealed_sender_allow_from_anyone">Allow from anyone</string>
|
||||
<string name="preferences_communication__sealed_sender_allow_from_anyone_description">Enable sealed sender for incoming messages from non-contacts and people with whom you have not shared your profile.</string>
|
||||
<string name="preferences_communication__sealed_sender_learn_more">Learn more</string>
|
||||
<string name="preferences_notifications__mentions">Mentions</string>
|
||||
<string name="preferences_notifications__notify_me">Notify me</string>
|
||||
<string name="preferences_notifications__receive_notifications_when_youre_mentioned_in_muted_chats">Receive notifications when you’re mentioned in muted chats</string>
|
||||
|
||||
<!-- Internal only preferences -->
|
||||
<string name="preferences__internal_preferences" translatable="false">Internal Preferences</string>
|
||||
|
|
|
@ -263,6 +263,7 @@
|
|||
<item name="conversation_title_color">@color/white</item>
|
||||
<item name="conversation_subtitle_color">@color/transparent_white_90</item>
|
||||
<item name="conversation_mention_background_color">@color/core_grey_20</item>
|
||||
<item name="conversation_mention_divider_color">@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>
|
||||
|
@ -622,6 +623,7 @@
|
|||
<item name="conversation_title_color">@color/transparent_white_90</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_mention_divider_color">@color/core_grey_25</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>
|
||||
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceCategory android:layout="@layout/preference_divider"/>
|
||||
|
||||
<PreferenceCategory android:title="@string/preferences_notifications__mentions">
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:key="notifications.mentions.notify_me"
|
||||
android:title="@string/preferences_notifications__notify_me"
|
||||
android:summary="@string/preferences_notifications__receive_notifications_when_youre_mentioned_in_muted_chats"
|
||||
android:defaultValue="true" />
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
|
@ -0,0 +1,121 @@
|
|||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner;
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_PLACEHOLDER;
|
||||
import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER;
|
||||
|
||||
@RunWith(ParameterizedRobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE, application = Application.class)
|
||||
public class MentionUtilTest_updateBodyAndMentionsWithPlaceholders {
|
||||
|
||||
private final String body;
|
||||
private final List<Mention> mentions;
|
||||
private final String updatedBody;
|
||||
private final List<Mention> updatedMentions;
|
||||
|
||||
@Parameters
|
||||
public static Collection<Object[]> data() {
|
||||
return Arrays.asList(new Object[][]{
|
||||
/* Empty states */
|
||||
{ null, Collections.emptyList(), null, Collections.emptyList() },
|
||||
builder().text("no mentions").build(),
|
||||
builder().text("").build(),
|
||||
builder().text("no mentions but @tester text").build(),
|
||||
|
||||
/* Singles */
|
||||
builder().mention("test").text(" start").build(),
|
||||
builder().text("middle ").mention("test").text(" middle").build(),
|
||||
builder().text("end end ").mention("test").build(),
|
||||
builder().mention("test").build(),
|
||||
|
||||
/* Doubles */
|
||||
builder().mention("foo").text(" ").mention("barbaz").build(),
|
||||
builder().text("start text ").mention("barbazbuzz").text(" ").mention("barbaz").build(),
|
||||
builder().text("what what ").mention("foo").text(" ").mention("barbaz").text(" more text").build(),
|
||||
builder().mention("barbazbuzz").text(" ").mention("foo").build(),
|
||||
|
||||
/* Triples */
|
||||
builder().mention("test").text(" ").mention("test2").text(" ").mention("test3").build(),
|
||||
builder().text("Starting ").mention("test").text(" ").mention("test2").text(" ").mention("test3").build(),
|
||||
builder().mention("test").text(" ").mention("test2").text(" ").mention("test3").text(" ending").build(),
|
||||
builder().mention("test").text(" ").mention("test2").text(" ").mention("test3").build(),
|
||||
builder().mention("no").mention("spaces").mention("atall").build(),
|
||||
|
||||
/* Emojis and Spaces */
|
||||
builder().mention("test").text(" start 🤘").build(),
|
||||
builder().mention("test").text(" start 🤘🤘").build(),
|
||||
builder().mention("test").text(" start 👍🏾").build(),
|
||||
builder().text("middle 🤡 ").mention("foo").text(" 👍🏾 middle").build(),
|
||||
builder().text("middle 🤡👍🏾 ").mention("test").text(" 👍🏾 middle").build(),
|
||||
builder().text("end end 💀 💀 💀 ").mention("bar baz buzz").build(),
|
||||
builder().text("end end 🖖🏼 🖖🏼 🖖🏼 ").mention("really long name").build(),
|
||||
builder().text("middle 🤡👍🏾 👨🏼🤝👨🏽 ").mention("a").text(" 👍🏾 middle 👩👩👦👦").build(),
|
||||
builder().text("start ").mention("emoji 🩳").build(),
|
||||
builder().text("start ").mention("emoji 🩳").text(" middle ").mention("emoji 🩳").text(" end").build(),
|
||||
});
|
||||
}
|
||||
|
||||
public MentionUtilTest_updateBodyAndMentionsWithPlaceholders(String body, List<Mention> mentions, String updatedBody, List<Mention> updatedMentions) {
|
||||
this.body = body;
|
||||
this.mentions = mentions;
|
||||
this.updatedBody = updatedBody;
|
||||
this.updatedMentions = updatedMentions;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateBodyAndMentions() {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithPlaceholders(body, mentions);
|
||||
assertEquals(updatedBody, updated.getBodyAsString());
|
||||
assertEquals(updatedMentions, updated.getMentions());
|
||||
}
|
||||
|
||||
private static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
private static class Builder {
|
||||
private StringBuilder bodyBuilder = new StringBuilder();
|
||||
private StringBuilder expectedBuilder = new StringBuilder();
|
||||
|
||||
private List<Mention> mentions = new ArrayList<>();
|
||||
private List<Mention> expectedMentions = new ArrayList<>();
|
||||
|
||||
Builder text(String text) {
|
||||
bodyBuilder.append(text);
|
||||
expectedBuilder.append(text);
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder mention(String name) {
|
||||
Mention input = new Mention(RecipientId.from(new Random().nextLong()), bodyBuilder.length(), name.length() + 1);
|
||||
bodyBuilder.append(MENTION_STARTER).append(name);
|
||||
mentions.add(input);
|
||||
|
||||
Mention output = new Mention(input.getRecipientId(), expectedBuilder.length(), MENTION_PLACEHOLDER.length());
|
||||
expectedBuilder.append(MENTION_PLACEHOLDER);
|
||||
expectedMentions.add(output);
|
||||
return this;
|
||||
}
|
||||
|
||||
Object[] build() {
|
||||
return new Object[]{ bodyBuilder.toString(), mentions, expectedBuilder.toString(), expectedMentions };
|
||||
}
|
||||
}
|
||||
}
|
|
@ -578,11 +578,22 @@ public class SignalServiceMessageSender {
|
|||
.setText(message.getQuote().get().getText());
|
||||
|
||||
if (message.getQuote().get().getAuthor().getUuid().isPresent()) {
|
||||
quoteBuilder = quoteBuilder.setAuthorUuid(message.getQuote().get().getAuthor().getUuid().get().toString());
|
||||
quoteBuilder.setAuthorUuid(message.getQuote().get().getAuthor().getUuid().get().toString());
|
||||
}
|
||||
|
||||
if (message.getQuote().get().getAuthor().getNumber().isPresent()) {
|
||||
quoteBuilder = quoteBuilder.setAuthorE164(message.getQuote().get().getAuthor().getNumber().get());
|
||||
quoteBuilder.setAuthorE164(message.getQuote().get().getAuthor().getNumber().get());
|
||||
}
|
||||
|
||||
if (!message.getQuote().get().getMentions().isEmpty()) {
|
||||
for (SignalServiceDataMessage.Mention mention : message.getQuote().get().getMentions()) {
|
||||
quoteBuilder.addBodyRanges(DataMessage.BodyRange.newBuilder()
|
||||
.setStart(mention.getStart())
|
||||
.setLength(mention.getLength())
|
||||
.setMentionUuid(mention.getUuid().toString()));
|
||||
}
|
||||
|
||||
builder.setRequiredProtocolVersion(Math.max(DataMessage.ProtocolVersion.MENTIONS_VALUE, builder.getRequiredProtocolVersion()));
|
||||
}
|
||||
|
||||
for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : message.getQuote().get().getAttachments()) {
|
||||
|
@ -626,6 +637,16 @@ public class SignalServiceMessageSender {
|
|||
}
|
||||
}
|
||||
|
||||
if (message.getMentions().isPresent()) {
|
||||
for (SignalServiceDataMessage.Mention mention : message.getMentions().get()) {
|
||||
builder.addBodyRanges(DataMessage.BodyRange.newBuilder()
|
||||
.setStart(mention.getStart())
|
||||
.setLength(mention.getLength())
|
||||
.setMentionUuid(mention.getUuid().toString()));
|
||||
}
|
||||
builder.setRequiredProtocolVersion(Math.max(DataMessage.ProtocolVersion.MENTIONS_VALUE, builder.getRequiredProtocolVersion()));
|
||||
}
|
||||
|
||||
if (message.getSticker().isPresent()) {
|
||||
DataMessage.Sticker.Builder stickerBuilder = DataMessage.Sticker.newBuilder();
|
||||
|
||||
|
|
|
@ -347,6 +347,7 @@ public final class SignalServiceContent {
|
|||
SignalServiceDataMessage.Quote quote = createQuote(content);
|
||||
List<SharedContact> sharedContacts = createSharedContacts(content);
|
||||
List<SignalServiceDataMessage.Preview> previews = createPreviews(content);
|
||||
List<SignalServiceDataMessage.Mention> mentions = createMentions(content);
|
||||
SignalServiceDataMessage.Sticker sticker = createSticker(content);
|
||||
SignalServiceDataMessage.Reaction reaction = createReaction(content);
|
||||
SignalServiceDataMessage.RemoteDelete remoteDelete = createRemoteDelete(content);
|
||||
|
@ -381,6 +382,7 @@ public final class SignalServiceContent {
|
|||
quote,
|
||||
sharedContacts,
|
||||
previews,
|
||||
mentions,
|
||||
sticker,
|
||||
content.getIsViewOnce(),
|
||||
reaction,
|
||||
|
@ -662,7 +664,8 @@ public final class SignalServiceContent {
|
|||
return new SignalServiceDataMessage.Quote(content.getQuote().getId(),
|
||||
address,
|
||||
content.getQuote().getText(),
|
||||
attachments);
|
||||
attachments,
|
||||
createMentions(content));
|
||||
} else {
|
||||
Log.w(TAG, "Quote was missing an author! Returning null.");
|
||||
return null;
|
||||
|
@ -689,6 +692,35 @@ public final class SignalServiceContent {
|
|||
return results;
|
||||
}
|
||||
|
||||
private static List<SignalServiceDataMessage.Mention> createMentions(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
|
||||
if (content.getBodyRangesCount() <= 0 || !content.hasBody()) return null;
|
||||
|
||||
List<SignalServiceDataMessage.Mention> mentions = new LinkedList<>();
|
||||
|
||||
for (SignalServiceProtos.DataMessage.BodyRange bodyRange : content.getBodyRangesList()) {
|
||||
if (bodyRange.hasMentionUuid()) {
|
||||
try {
|
||||
validateBodyRange(content, bodyRange);
|
||||
mentions.add(new SignalServiceDataMessage.Mention(UuidUtil.parseOrThrow(bodyRange.getMentionUuid()), bodyRange.getStart(), bodyRange.getLength()));
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ProtocolInvalidMessageException(new InvalidMessageException(e), null, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mentions;
|
||||
}
|
||||
|
||||
private static void validateBodyRange(SignalServiceProtos.DataMessage content, SignalServiceProtos.DataMessage.BodyRange bodyRange) throws ProtocolInvalidMessageException {
|
||||
int incomingBodyLength = content.hasBody() ? content.getBody().length() : -1;
|
||||
int start = bodyRange.hasStart() ? bodyRange.getStart() : -1;
|
||||
int length = bodyRange.hasLength() ? bodyRange.getLength() : -1;
|
||||
|
||||
if (start < 0 || length < 0 || (start + length) > incomingBodyLength) {
|
||||
throw new ProtocolInvalidMessageException(new InvalidMessageException("Incoming body range has out-of-bound range"), null, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private static SignalServiceDataMessage.Sticker createSticker(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
|
||||
if (!content.hasSticker() ||
|
||||
!content.getSticker().hasPackId() ||
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.whispersystems.signalservice.api.util.OptionalUtil;
|
|||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Represents a decrypted Signal Service data message.
|
||||
|
@ -32,6 +33,7 @@ public class SignalServiceDataMessage {
|
|||
private final Optional<Quote> quote;
|
||||
private final Optional<List<SharedContact>> contacts;
|
||||
private final Optional<List<Preview>> previews;
|
||||
private final Optional<List<Mention>> mentions;
|
||||
private final Optional<Sticker> sticker;
|
||||
private final boolean viewOnce;
|
||||
private final Optional<Reaction> reaction;
|
||||
|
@ -54,7 +56,7 @@ public class SignalServiceDataMessage {
|
|||
String body, boolean endSession, int expiresInSeconds,
|
||||
boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate,
|
||||
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
|
||||
Sticker sticker, boolean viewOnce, Reaction reaction, RemoteDelete remoteDelete)
|
||||
List<Mention> mentions, Sticker sticker, boolean viewOnce, Reaction reaction, RemoteDelete remoteDelete)
|
||||
{
|
||||
try {
|
||||
this.group = SignalServiceGroupContext.createOptional(group, groupV2);
|
||||
|
@ -92,6 +94,12 @@ public class SignalServiceDataMessage {
|
|||
} else {
|
||||
this.previews = Optional.absent();
|
||||
}
|
||||
|
||||
if (mentions != null && !mentions.isEmpty()) {
|
||||
this.mentions = Optional.of(mentions);
|
||||
} else {
|
||||
this.mentions = Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
public static Builder newBuilder() {
|
||||
|
@ -174,6 +182,10 @@ public class SignalServiceDataMessage {
|
|||
return previews;
|
||||
}
|
||||
|
||||
public Optional<List<Mention>> getMentions() {
|
||||
return mentions;
|
||||
}
|
||||
|
||||
public Optional<Sticker> getSticker() {
|
||||
return sticker;
|
||||
}
|
||||
|
@ -195,6 +207,7 @@ public class SignalServiceDataMessage {
|
|||
private List<SignalServiceAttachment> attachments = new LinkedList<>();
|
||||
private List<SharedContact> sharedContacts = new LinkedList<>();
|
||||
private List<Preview> previews = new LinkedList<>();
|
||||
private List<Mention> mentions = new LinkedList<>();
|
||||
|
||||
private long timestamp;
|
||||
private SignalServiceGroup group;
|
||||
|
@ -302,6 +315,11 @@ public class SignalServiceDataMessage {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder withMentions(List<Mention> mentions) {
|
||||
this.mentions.addAll(mentions);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withSticker(Sticker sticker) {
|
||||
this.sticker = sticker;
|
||||
return this;
|
||||
|
@ -327,7 +345,7 @@ public class SignalServiceDataMessage {
|
|||
return new SignalServiceDataMessage(timestamp, group, groupV2, attachments, body, endSession,
|
||||
expiresInSeconds, expirationUpdate, profileKey,
|
||||
profileKeyUpdate, quote, sharedContacts, previews,
|
||||
sticker, viewOnce, reaction, remoteDelete);
|
||||
mentions, sticker, viewOnce, reaction, remoteDelete);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -336,12 +354,14 @@ public class SignalServiceDataMessage {
|
|||
private final SignalServiceAddress author;
|
||||
private final String text;
|
||||
private final List<QuotedAttachment> attachments;
|
||||
private final List<Mention> mentions;
|
||||
|
||||
public Quote(long id, SignalServiceAddress author, String text, List<QuotedAttachment> attachments) {
|
||||
public Quote(long id, SignalServiceAddress author, String text, List<QuotedAttachment> attachments, List<Mention> mentions) {
|
||||
this.id = id;
|
||||
this.author = author;
|
||||
this.text = text;
|
||||
this.attachments = attachments;
|
||||
this.mentions = mentions;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
|
@ -360,6 +380,10 @@ public class SignalServiceDataMessage {
|
|||
return attachments;
|
||||
}
|
||||
|
||||
public List<Mention> getMentions() {
|
||||
return mentions;
|
||||
}
|
||||
|
||||
public static class QuotedAttachment {
|
||||
private final String contentType;
|
||||
private final String fileName;
|
||||
|
@ -480,4 +504,28 @@ public class SignalServiceDataMessage {
|
|||
return targetSentTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Mention {
|
||||
private final UUID uuid;
|
||||
private final int start;
|
||||
private final int length;
|
||||
|
||||
public Mention(UUID uuid, int start, int length) {
|
||||
this.uuid = uuid;
|
||||
this.start = start;
|
||||
this.length = length;
|
||||
}
|
||||
|
||||
public UUID getUuid() {
|
||||
return uuid;
|
||||
}
|
||||
|
||||
public int getStart() {
|
||||
return start;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -111,6 +111,15 @@ message DataMessage {
|
|||
PROFILE_KEY_UPDATE = 4;
|
||||
}
|
||||
|
||||
message BodyRange {
|
||||
optional int32 start = 1;
|
||||
optional int32 length = 2;
|
||||
|
||||
oneof associatedValue {
|
||||
string mentionUuid = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message Quote {
|
||||
message QuotedAttachment {
|
||||
optional string contentType = 1;
|
||||
|
@ -123,6 +132,7 @@ message DataMessage {
|
|||
optional string authorUuid = 5;
|
||||
optional string text = 3;
|
||||
repeated QuotedAttachment attachments = 4;
|
||||
repeated BodyRange bodyRanges = 6;
|
||||
}
|
||||
|
||||
message Contact {
|
||||
|
@ -226,7 +236,8 @@ message DataMessage {
|
|||
VIEW_ONCE_VIDEO = 3;
|
||||
REACTIONS = 4;
|
||||
CDN_SELECTOR_ATTACHMENTS = 5;
|
||||
CURRENT = 5;
|
||||
MENTIONS = 6;
|
||||
CURRENT = 6;
|
||||
}
|
||||
|
||||
optional string body = 1;
|
||||
|
@ -245,6 +256,7 @@ message DataMessage {
|
|||
optional bool isViewOnce = 14;
|
||||
optional Reaction reaction = 16;
|
||||
optional Delete delete = 17;
|
||||
repeated BodyRange bodyRanges = 18;
|
||||
}
|
||||
|
||||
message NullMessage {
|
||||
|
|
Loading…
Reference in New Issue