kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add text formatting send and receive support for conversations.
rodzic
aa2075c78f
commit
cc490f4b73
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.components;
|
|||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Annotation;
|
||||
|
@ -14,8 +15,15 @@ import android.text.SpannableStringBuilder;
|
|||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextUtils.TruncateAt;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.text.style.StrikethroughSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
|
@ -35,18 +43,19 @@ 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.conversation.MessageSendType;
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQuery;
|
||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryChangedListener;
|
||||
import org.thoughtcrime.securesms.conversation.ui.inlinequery.InlineQueryReplacement;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.thoughtcrime.securesms.database.MentionUtil.MENTION_STARTER;
|
||||
|
@ -276,6 +285,19 @@ public class ComposeText extends EmojiEditText {
|
|||
return MentionAnnotation.getMentionsFromAnnotations(getText());
|
||||
}
|
||||
|
||||
public boolean hasStyling() {
|
||||
CharSequence trimmed = getTextTrimmed();
|
||||
return FeatureFlags.textFormatting() && (trimmed instanceof Spanned) && MessageStyler.hasStyling((Spanned) trimmed);
|
||||
}
|
||||
|
||||
public @Nullable BodyRangeList getStyling() {
|
||||
if (FeatureFlags.textFormatting()) {
|
||||
return MessageStyler.getStyling(getTextTrimmed());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
if (TextSecurePreferences.isIncognitoKeyboardEnabled(getContext())) {
|
||||
setImeOptions(getImeOptions() | 16777216);
|
||||
|
@ -286,6 +308,80 @@ public class ComposeText extends EmojiEditText {
|
|||
addTextChangedListener(new MentionDeleter());
|
||||
mentionValidatorWatcher = new MentionValidatorWatcher();
|
||||
addTextChangedListener(mentionValidatorWatcher);
|
||||
|
||||
if (FeatureFlags.textFormatting()) {
|
||||
setCustomSelectionActionModeCallback(new ActionMode.Callback() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
MenuItem copy = menu.findItem(android.R.id.copy);
|
||||
MenuItem cut = menu.findItem(android.R.id.cut);
|
||||
MenuItem paste = menu.findItem(android.R.id.paste);
|
||||
int copyOrder = copy != null ? copy.getOrder() : 0;
|
||||
int cutOrder = cut != null ? cut.getOrder() : 0;
|
||||
int pasteOrder = paste != null ? paste.getOrder() : 0;
|
||||
int largestOrder = Math.max(copyOrder, Math.max(cutOrder, pasteOrder));
|
||||
|
||||
menu.add(0, R.id.edittext_bold, largestOrder, getContext().getString(R.string.TextFormatting_bold));
|
||||
menu.add(0, R.id.edittext_italic, largestOrder, getContext().getString(R.string.TextFormatting_italic));
|
||||
menu.add(0, R.id.edittext_strikethrough, largestOrder, getContext().getString(R.string.TextFormatting_strikethrough));
|
||||
menu.add(0, R.id.edittext_monospace, largestOrder, getContext().getString(R.string.TextFormatting_monospace));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
Editable text = getText();
|
||||
|
||||
if (text == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (item.getItemId() != R.id.edittext_bold &&
|
||||
item.getItemId() != R.id.edittext_italic &&
|
||||
item.getItemId() != R.id.edittext_strikethrough &&
|
||||
item.getItemId() != R.id.edittext_monospace) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int start = getSelectionStart();
|
||||
int end = getSelectionEnd();
|
||||
|
||||
CharSequence charSequence = text.subSequence(start, end);
|
||||
SpannableString replacement = new SpannableString(charSequence);
|
||||
CharacterStyle style = null;
|
||||
|
||||
if (item.getItemId() == R.id.edittext_bold) {
|
||||
style = MessageStyler.boldStyle();
|
||||
} else if (item.getItemId() == R.id.edittext_italic) {
|
||||
style = MessageStyler.italicStyle();
|
||||
} else if (item.getItemId() == R.id.edittext_strikethrough) {
|
||||
style = MessageStyler.strikethroughStyle();
|
||||
} else if (item.getItemId() == R.id.edittext_monospace) {
|
||||
style = MessageStyler.monoStyle();
|
||||
}
|
||||
|
||||
if (style != null) {
|
||||
replacement.setSpan(style, 0, charSequence.length(), Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
|
||||
}
|
||||
|
||||
clearComposingText();
|
||||
|
||||
text.replace(start, end, replacement);
|
||||
|
||||
mode.finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void setHintWithChecks(@Nullable CharSequence newHint) {
|
||||
|
|
|
@ -268,7 +268,14 @@ 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().toString(), false, quoteView.getAttachments(), quoteView.getMentions(), quoteView.getQuoteType()));
|
||||
return Optional.of(new QuoteModel(quoteView.getQuoteId(),
|
||||
quoteView.getAuthor().getId(),
|
||||
quoteView.getBody().toString(),
|
||||
false,
|
||||
quoteView.getAttachments(),
|
||||
quoteView.getMentions(),
|
||||
quoteView.getQuoteType(),
|
||||
quoteView.getBodyRanges()));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
|
|
@ -29,7 +29,9 @@ import org.thoughtcrime.securesms.attachments.Attachment;
|
|||
import org.thoughtcrime.securesms.components.emoji.EmojiImageView;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.components.quotes.QuoteViewColorTheme;
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||
|
@ -437,7 +439,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
}
|
||||
|
||||
try {
|
||||
return StoryTextPostModel.parseFrom(body.toString(), id, author.getId());
|
||||
return StoryTextPostModel.parseFrom(body.toString(), id, author.getId(), MessageStyler.getStyling(body));
|
||||
} catch (IOException ioException) {
|
||||
return null;
|
||||
}
|
||||
|
@ -471,6 +473,10 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
|||
return MentionAnnotation.getMentionsFromAnnotations(body);
|
||||
}
|
||||
|
||||
public @Nullable BodyRangeList getBodyRanges() {
|
||||
return MessageStyler.getStyling(body);
|
||||
}
|
||||
|
||||
private @NonNull ShapeAppearanceModel buildShapeAppearanceForLayoutDirection() {
|
||||
int fourDp = (int) DimensionUnit.DP.toPixels(4);
|
||||
if (getLayoutDirection() == LAYOUT_DIRECTION_LTR) {
|
||||
|
|
|
@ -166,7 +166,7 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
|||
stopwatch.split("recipient-resolves");
|
||||
|
||||
List<ConversationMessage> messages = Stream.of(records)
|
||||
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId()), quotedHelper.isQuoted(m.getId())))
|
||||
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, m.getDisplayBody(context), mentionHelper.getMentions(m.getId()), quotedHelper.isQuoted(m.getId())))
|
||||
.toList();
|
||||
|
||||
stopwatch.split("conversion");
|
||||
|
@ -220,7 +220,11 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
|||
|
||||
stopwatch.split("calls");
|
||||
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(), record, mentions, isQuoted);
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(),
|
||||
record,
|
||||
record.getDisplayBody(ApplicationDependencies.getApplication()),
|
||||
mentions,
|
||||
isQuoted);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -181,6 +181,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
private static final long MAX_CLUSTERING_TIME_DIFF = TimeUnit.MINUTES.toMillis(3);
|
||||
private static final int CONDENSED_MODE_MAX_LINES = 3;
|
||||
|
||||
private static final SearchUtil.StyleFactory STYLE_FACTORY = () -> new CharacterStyle[] { new BackgroundColorSpan(Color.YELLOW), new ForegroundColorSpan(Color.BLACK) };
|
||||
|
||||
private ConversationMessage conversationMessage;
|
||||
private MessageRecord messageRecord;
|
||||
private Optional<MessageRecord> nextMessageRecord;
|
||||
|
@ -988,8 +990,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
if (messageRequestAccepted) {
|
||||
linkifyMessageBody(styledText, batchSelected.isEmpty());
|
||||
}
|
||||
styledText = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), styledText, searchQuery, SearchUtil.STRICT);
|
||||
styledText = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), styledText, searchQuery, SearchUtil.STRICT);
|
||||
styledText = SearchUtil.getHighlightedSpan(locale, STYLE_FACTORY, styledText, searchQuery, SearchUtil.STRICT);
|
||||
|
||||
if (hasExtraText(messageRecord)) {
|
||||
bodyText.setOverflowText(getLongMessageSpan(messageRecord));
|
||||
|
|
|
@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation;
|
|||
import android.content.Context;
|
||||
import android.text.SpannableString;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
@ -12,6 +11,7 @@ import org.signal.core.util.Conversions;
|
|||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselect;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectCollection;
|
||||
import org.thoughtcrime.securesms.database.BodyRangeUtil;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
|
@ -35,22 +35,20 @@ public class ConversationMessage {
|
|||
@NonNull private final MessageStyler.Result styleResult;
|
||||
private final boolean hasBeenQuoted;
|
||||
|
||||
private ConversationMessage(@NonNull MessageRecord messageRecord, boolean hasBeenQuoted) {
|
||||
this(messageRecord, null, null, hasBeenQuoted);
|
||||
}
|
||||
|
||||
private ConversationMessage(@NonNull MessageRecord messageRecord,
|
||||
@Nullable CharSequence body,
|
||||
@Nullable List<Mention> mentions,
|
||||
boolean hasBeenQuoted)
|
||||
boolean hasBeenQuoted,
|
||||
@Nullable MessageStyler.Result styleResult)
|
||||
{
|
||||
this.messageRecord = messageRecord;
|
||||
this.hasBeenQuoted = hasBeenQuoted;
|
||||
this.mentions = mentions != null ? mentions : Collections.emptyList();
|
||||
this.styleResult = styleResult != null ? styleResult : MessageStyler.Result.none();
|
||||
|
||||
if (body != null) {
|
||||
this.body = SpannableString.valueOf(body);
|
||||
} else if (messageRecord.hasMessageRanges()) {
|
||||
} else if (messageRecord.getMessageRanges() != null) {
|
||||
this.body = SpannableString.valueOf(messageRecord.getBody());
|
||||
} else {
|
||||
this.body = null;
|
||||
|
@ -60,12 +58,6 @@ public class ConversationMessage {
|
|||
MentionAnnotation.setMentionAnnotations(this.body, this.mentions);
|
||||
}
|
||||
|
||||
if (this.body != null && messageRecord.hasMessageRanges()) {
|
||||
styleResult = MessageStyler.style(messageRecord.requireMessageRanges(), this.body);
|
||||
} else {
|
||||
styleResult = MessageStyler.Result.none();
|
||||
}
|
||||
|
||||
multiselectCollection = Multiselect.getParts(this);
|
||||
}
|
||||
|
||||
|
@ -128,32 +120,6 @@ public class ConversationMessage {
|
|||
*/
|
||||
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, boolean hasBeenQuoted) {
|
||||
return new ConversationMessage(messageRecord, hasBeenQuoted);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param hasBeenQuoted Whether or not the message has been quoted by another message.
|
||||
*/
|
||||
@AnyThread
|
||||
public static @NonNull ConversationMessage createWithResolvedData(@NonNull MessageRecord messageRecord, @Nullable CharSequence body, @Nullable List<Mention> mentions, boolean hasBeenQuoted) {
|
||||
if (messageRecord.isMms() && mentions != null && !mentions.isEmpty()) {
|
||||
return new ConversationMessage(messageRecord, body, mentions, hasBeenQuoted);
|
||||
}
|
||||
return new ConversationMessage(messageRecord, body, null, hasBeenQuoted);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@ -161,12 +127,33 @@ public class ConversationMessage {
|
|||
* @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, boolean hasBeenQuoted) {
|
||||
if (messageRecord.isMms() && mentions != null && !mentions.isEmpty()) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, messageRecord, mentions);
|
||||
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions(), hasBeenQuoted);
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context,
|
||||
@NonNull MessageRecord messageRecord,
|
||||
@NonNull CharSequence body,
|
||||
@Nullable List<Mention> mentions,
|
||||
boolean hasBeenQuoted)
|
||||
{
|
||||
SpannableString styledAndMentionBody = null;
|
||||
MessageStyler.Result styleResult = MessageStyler.Result.none();
|
||||
|
||||
MentionUtil.UpdatedBodyAndMentions mentionsUpdate = null;
|
||||
if (mentions != null && !mentions.isEmpty()) {
|
||||
mentionsUpdate = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, body, mentions);
|
||||
}
|
||||
return createWithResolvedData(messageRecord, hasBeenQuoted);
|
||||
|
||||
if (messageRecord.getMessageRanges() != null) {
|
||||
BodyRangeList bodyRanges = mentionsUpdate == null ? messageRecord.getMessageRanges()
|
||||
: BodyRangeUtil.adjustBodyRanges(messageRecord.getMessageRanges(), mentionsUpdate.getBodyAdjustments());
|
||||
|
||||
styledAndMentionBody = SpannableString.valueOf(mentionsUpdate != null ? mentionsUpdate.getBody() : body);
|
||||
styleResult = MessageStyler.style(bodyRanges, styledAndMentionBody);
|
||||
}
|
||||
|
||||
return new ConversationMessage(messageRecord,
|
||||
styledAndMentionBody != null ? styledAndMentionBody : mentionsUpdate != null ? mentionsUpdate.getBody() : body,
|
||||
mentionsUpdate != null ? mentionsUpdate.getMentions() : null,
|
||||
hasBeenQuoted,
|
||||
styleResult);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -185,17 +172,10 @@ public class ConversationMessage {
|
|||
* 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, @NonNull CharSequence body) {
|
||||
boolean hasBeenQuoted = SignalDatabase.messages().isQuoted(messageRecord);
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId());
|
||||
if (!mentions.isEmpty()) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, body, mentions);
|
||||
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions(), hasBeenQuoted);
|
||||
}
|
||||
}
|
||||
return createWithResolvedData(messageRecord, body, null, hasBeenQuoted);
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, boolean hasBeenQuoted) {
|
||||
List<Mention> mentions = messageRecord.isMms() ? SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId())
|
||||
: null;
|
||||
return createWithUnresolvedData(context, messageRecord, messageRecord.getDisplayBody(context), mentions, hasBeenQuoted);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -204,15 +184,11 @@ public class ConversationMessage {
|
|||
* 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, @NonNull CharSequence body, boolean hasBeenQuoted) {
|
||||
if (messageRecord.isMms()) {
|
||||
List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId());
|
||||
if (!mentions.isEmpty()) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, body, mentions);
|
||||
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions(), hasBeenQuoted);
|
||||
}
|
||||
}
|
||||
return createWithResolvedData(messageRecord, body, null, hasBeenQuoted);
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body) {
|
||||
boolean hasBeenQuoted = SignalDatabase.messages().isQuoted(messageRecord);
|
||||
List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId());
|
||||
|
||||
return createWithUnresolvedData(context, messageRecord, body, mentions, hasBeenQuoted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -186,6 +186,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
|||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StoryType;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.GroupCallPeekEvent;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
|
@ -748,11 +749,12 @@ public class ConversationParentFragment extends Fragment
|
|||
return;
|
||||
}
|
||||
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
boolean initiating = threadId == -1;
|
||||
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orElse(null);
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
List<Mention> mentions = new ArrayList<>(result.getMentions());
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
boolean initiating = threadId == -1;
|
||||
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orElse(null);
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
List<Mention> mentions = new ArrayList<>(result.getMentions());
|
||||
BodyRangeList bodyRanges = result.getBodyRanges();
|
||||
|
||||
for (Media mediaItem : result.getNonUploadedMedia()) {
|
||||
if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
|
||||
|
@ -776,6 +778,7 @@ public class ConversationParentFragment extends Fragment
|
|||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
mentions,
|
||||
bodyRanges,
|
||||
expiresIn,
|
||||
result.isViewOnce(),
|
||||
initiating,
|
||||
|
@ -1840,7 +1843,7 @@ public class ConversationParentFragment extends Fragment
|
|||
quoteResult.addListener(listener);
|
||||
break;
|
||||
case Draft.VOICE_NOTE:
|
||||
case Draft.MENTION:
|
||||
case Draft.BODY_RANGES:
|
||||
listener.onSuccess(true);
|
||||
break;
|
||||
}
|
||||
|
@ -2704,7 +2707,7 @@ public class ConversationParentFragment extends Fragment
|
|||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
boolean initiating = threadId == -1;
|
||||
|
||||
sendMediaMessage(recipient.getId(), sendButton.getSelectedSendType(), "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), Collections.emptyList(), expiresIn, false, initiating, false, null);
|
||||
sendMediaMessage(recipient.getId(), sendButton.getSelectedSendType(), "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), Collections.emptyList(), null, expiresIn, false, initiating, false, null);
|
||||
}
|
||||
|
||||
private void selectContactInfo(ContactData contactData) {
|
||||
|
@ -2943,6 +2946,7 @@ public class ConversationParentFragment extends Fragment
|
|||
recipient.getEmail().isPresent() ||
|
||||
inputPanel.getQuote().isPresent() ||
|
||||
composeText.hasMentions() ||
|
||||
composeText.hasStyling() ||
|
||||
linkPreviewViewModel.hasLinkPreview() ||
|
||||
needsSplit;
|
||||
|
||||
|
@ -2997,9 +3001,10 @@ public class ConversationParentFragment extends Fragment
|
|||
Collections.emptySet(),
|
||||
Collections.emptySet(),
|
||||
null,
|
||||
true);
|
||||
true,
|
||||
result.getBodyRanges());
|
||||
|
||||
final Context context = requireContext().getApplicationContext();
|
||||
final Context context = requireContext().getApplicationContext();
|
||||
|
||||
ApplicationDependencies.getTypingStatusSender().onTypingStopped(thread);
|
||||
|
||||
|
@ -3032,6 +3037,7 @@ public class ConversationParentFragment extends Fragment
|
|||
Collections.emptyList(),
|
||||
linkPreviews,
|
||||
composeText.getMentions(),
|
||||
composeText.getStyling(),
|
||||
expiresIn,
|
||||
viewOnce,
|
||||
initiating,
|
||||
|
@ -3047,6 +3053,7 @@ public class ConversationParentFragment extends Fragment
|
|||
List<Contact> contacts,
|
||||
List<LinkPreview> previews,
|
||||
List<Mention> mentions,
|
||||
@Nullable BodyRangeList styling,
|
||||
final long expiresIn,
|
||||
final boolean viewOnce,
|
||||
final boolean initiating,
|
||||
|
@ -3093,7 +3100,8 @@ public class ConversationParentFragment extends Fragment
|
|||
Collections.emptySet(),
|
||||
Collections.emptySet(),
|
||||
null,
|
||||
false);
|
||||
false,
|
||||
styling);
|
||||
|
||||
final SettableFuture<Void> future = new SettableFuture<>();
|
||||
final Context context = requireContext().getApplicationContext();
|
||||
|
@ -3154,7 +3162,7 @@ public class ConversationParentFragment extends Fragment
|
|||
OutgoingMessage message;
|
||||
|
||||
if (sendPush) {
|
||||
message = OutgoingMessage.text(recipient.get(), messageBody, expiresIn, System.currentTimeMillis());
|
||||
message = OutgoingMessage.text(recipient.get(), messageBody, expiresIn, System.currentTimeMillis(), null);
|
||||
ApplicationDependencies.getTypingStatusSender().onTypingStopped(thread);
|
||||
} else {
|
||||
message = OutgoingMessage.sms(recipient.get(), messageBody, sendType.getSimSubscriptionIdOr(-1));
|
||||
|
@ -3411,6 +3419,7 @@ public class ConversationParentFragment extends Fragment
|
|||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
composeText.getMentions(),
|
||||
composeText.getStyling(),
|
||||
expiresIn,
|
||||
false,
|
||||
initiating,
|
||||
|
@ -3443,7 +3452,7 @@ public class ConversationParentFragment extends Fragment
|
|||
|
||||
slideDeck.addSlide(stickerSlide);
|
||||
|
||||
sendMediaMessage(recipient.getId(), sendType, "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), expiresIn, false, initiating, clearCompose, null);
|
||||
sendMediaMessage(recipient.getId(), sendType, "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), null, expiresIn, false, initiating, clearCompose, null);
|
||||
}
|
||||
|
||||
private void silentlySetComposeText(String text) {
|
||||
|
@ -3746,7 +3755,7 @@ public class ConversationParentFragment extends Fragment
|
|||
}
|
||||
|
||||
private void handleSaveDraftOnTextChange(@NonNull CharSequence text) {
|
||||
textDraftSaveDebouncer.publish(() -> draftViewModel.setTextDraft(StringUtil.trimSequence(text).toString(), MentionAnnotation.getMentionsFromAnnotations(text)));
|
||||
textDraftSaveDebouncer.publish(() -> draftViewModel.setTextDraft(StringUtil.trimSequence(text).toString(), MentionAnnotation.getMentionsFromAnnotations(text), MessageStyler.getStyling(text)));
|
||||
}
|
||||
|
||||
private void handleTypingIndicatorOnTextChange(@NonNull String text) {
|
||||
|
@ -4185,6 +4194,7 @@ public class ConversationParentFragment extends Fragment
|
|||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
composeText.getMentions(),
|
||||
composeText.getStyling(),
|
||||
expiresIn,
|
||||
false,
|
||||
initiating,
|
||||
|
|
|
@ -1,45 +1,133 @@
|
|||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.SpannableString
|
||||
import android.text.Spannable
|
||||
import android.text.Spanned
|
||||
import android.text.style.CharacterStyle
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.TypefaceSpan
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.PlaceholderURLSpan
|
||||
|
||||
/**
|
||||
* Helper for applying style-based [BodyRangeList.BodyRange]s to text.
|
||||
* Helper for parsing and applying styles. Most notably with [BodyRangeList].
|
||||
*/
|
||||
object MessageStyler {
|
||||
|
||||
const val MONOSPACE = "monospace"
|
||||
|
||||
@JvmStatic
|
||||
fun style(messageRanges: BodyRangeList, span: SpannableString): Result {
|
||||
fun boldStyle(): CharacterStyle {
|
||||
return StyleSpan(Typeface.BOLD)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun italicStyle(): CharacterStyle {
|
||||
return StyleSpan(Typeface.ITALIC)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun strikethroughStyle(): CharacterStyle {
|
||||
return StrikethroughSpan()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun monoStyle(): CharacterStyle {
|
||||
return TypefaceSpan(MONOSPACE)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun style(messageRanges: BodyRangeList?, span: Spannable): Result {
|
||||
if (messageRanges == null) {
|
||||
return Result.none()
|
||||
}
|
||||
|
||||
var appliedStyle = false
|
||||
var hasLinks = false
|
||||
var bottomButton: BodyRangeList.BodyRange.Button? = null
|
||||
|
||||
for (range in messageRanges.rangesList) {
|
||||
if (range.hasStyle()) {
|
||||
messageRanges
|
||||
.rangesList
|
||||
.filter { r -> r.start >= 0 && r.start < span.length && r.start + r.length >= 0 && r.start + r.length <= span.length }
|
||||
.forEach { range ->
|
||||
if (range.hasStyle()) {
|
||||
val styleSpan: CharacterStyle? = when (range.style) {
|
||||
BodyRangeList.BodyRange.Style.BOLD -> boldStyle()
|
||||
BodyRangeList.BodyRange.Style.ITALIC -> italicStyle()
|
||||
BodyRangeList.BodyRange.Style.STRIKETHROUGH -> strikethroughStyle()
|
||||
BodyRangeList.BodyRange.Style.MONOSPACE -> monoStyle()
|
||||
else -> null
|
||||
}
|
||||
|
||||
val styleSpan: CharacterStyle? = when (range.style) {
|
||||
BodyRangeList.BodyRange.Style.BOLD -> TypefaceSpan("sans-serif-medium")
|
||||
BodyRangeList.BodyRange.Style.ITALIC -> StyleSpan(Typeface.ITALIC)
|
||||
else -> null
|
||||
if (styleSpan != null) {
|
||||
span.setSpan(styleSpan, range.start, range.start + range.length, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
|
||||
appliedStyle = true
|
||||
}
|
||||
} else if (range.hasLink() && range.link != null) {
|
||||
span.setSpan(PlaceholderURLSpan(range.link), range.start, range.start + range.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
hasLinks = true
|
||||
} else if (range.hasButton() && range.button != null) {
|
||||
bottomButton = range.button
|
||||
}
|
||||
|
||||
if (styleSpan != null) {
|
||||
span.setSpan(styleSpan, range.start, range.start + range.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
} else if (range.hasLink() && range.link != null) {
|
||||
span.setSpan(PlaceholderURLSpan(range.link), range.start, range.start + range.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
hasLinks = true
|
||||
} else if (range.hasButton() && range.button != null) {
|
||||
bottomButton = range.button
|
||||
}
|
||||
|
||||
return if (appliedStyle || hasLinks || bottomButton != null) {
|
||||
Result(hasLinks, bottomButton)
|
||||
} else {
|
||||
Result.none()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasStyling(text: Spanned): Boolean {
|
||||
return if (FeatureFlags.textFormatting()) {
|
||||
text.getSpans(0, text.length, CharacterStyle::class.java)
|
||||
.any { s -> isSupportedCharacterStyle(s) }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getStyling(text: CharSequence?): BodyRangeList? {
|
||||
val bodyRanges = if (text is Spanned && FeatureFlags.textFormatting()) {
|
||||
text
|
||||
.getSpans(0, text.length, CharacterStyle::class.java)
|
||||
.filter { s -> isSupportedCharacterStyle(s) }
|
||||
.map { span: CharacterStyle ->
|
||||
val spanStart = text.getSpanStart(span)
|
||||
val spanLength = text.getSpanEnd(span) - spanStart
|
||||
|
||||
val style = when (span) {
|
||||
is StyleSpan -> if (span.style == Typeface.BOLD) BodyRangeList.BodyRange.Style.BOLD else BodyRangeList.BodyRange.Style.ITALIC
|
||||
is StrikethroughSpan -> BodyRangeList.BodyRange.Style.STRIKETHROUGH
|
||||
is TypefaceSpan -> BodyRangeList.BodyRange.Style.MONOSPACE
|
||||
else -> throw IllegalArgumentException("Provided text contains unsupported spans")
|
||||
}
|
||||
|
||||
BodyRangeList.BodyRange.newBuilder().setStart(spanStart).setLength(spanLength).setStyle(style).build()
|
||||
}
|
||||
.toList()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
return Result(hasLinks, bottomButton)
|
||||
return if (bodyRanges.isNotEmpty()) {
|
||||
BodyRangeList.newBuilder().addAllRanges(bodyRanges).build()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSupportedCharacterStyle(style: CharacterStyle): Boolean {
|
||||
return when (style) {
|
||||
is StyleSpan -> style.style == Typeface.ITALIC || style.style == Typeface.BOLD
|
||||
is StrikethroughSpan -> true
|
||||
is TypefaceSpan -> style.family == MONOSPACE
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
data class Result(val hasStyleLinks: Boolean = false, val bottomButton: BodyRangeList.BodyRange.Button? = null) {
|
||||
|
|
|
@ -8,12 +8,16 @@ import io.reactivex.rxjava3.core.Single
|
|||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler
|
||||
import org.thoughtcrime.securesms.database.DraftTable
|
||||
import org.thoughtcrime.securesms.database.DraftTable.Drafts
|
||||
import org.thoughtcrime.securesms.database.MentionUtil
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.database.adjustBodyRanges
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
@ -57,15 +61,18 @@ class DraftRepository(
|
|||
fun loadDrafts(threadId: Long): Single<DatabaseDraft> {
|
||||
return Single.fromCallable {
|
||||
val drafts: Drafts = draftTable.getDrafts(threadId)
|
||||
val mentionsDraft = drafts.getDraftOfType(DraftTable.Draft.MENTION)
|
||||
val bodyRangesDraft = drafts.getDraftOfType(DraftTable.Draft.BODY_RANGES)
|
||||
var updatedText: Spannable? = null
|
||||
|
||||
if (mentionsDraft != null) {
|
||||
val text = drafts.getDraftOfType(DraftTable.Draft.TEXT)!!.value
|
||||
val mentions = MentionUtil.bodyRangeListToMentions(context, Base64.decodeOrThrow(mentionsDraft.value))
|
||||
val updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, text, mentions)
|
||||
if (bodyRangesDraft != null) {
|
||||
val bodyRanges: BodyRangeList = BodyRangeList.parseFrom(Base64.decodeOrThrow(bodyRangesDraft.value))
|
||||
val mentions: List<Mention> = MentionUtil.bodyRangeListToMentions(bodyRanges)
|
||||
|
||||
val updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, drafts.getDraftOfType(DraftTable.Draft.TEXT)!!.value, mentions)
|
||||
|
||||
updatedText = SpannableString(updated.body)
|
||||
MentionAnnotation.setMentionAnnotations(updatedText, updated.mentions)
|
||||
MessageStyler.style(bodyRanges.adjustBodyRanges(updated.bodyAdjustments), updatedText)
|
||||
}
|
||||
|
||||
DatabaseDraft(drafts, updatedText)
|
||||
|
|
|
@ -14,7 +14,7 @@ data class DraftState(
|
|||
val threadId: Long = -1,
|
||||
val distributionType: Int = 0,
|
||||
val textDraft: DraftTable.Draft? = null,
|
||||
val mentionsDraft: DraftTable.Draft? = null,
|
||||
val bodyRangesDraft: DraftTable.Draft? = null,
|
||||
val quoteDraft: DraftTable.Draft? = null,
|
||||
val locationDraft: DraftTable.Draft? = null,
|
||||
val voiceNoteDraft: DraftTable.Draft? = null,
|
||||
|
@ -27,7 +27,7 @@ data class DraftState(
|
|||
fun toDrafts(): Drafts {
|
||||
return Drafts().apply {
|
||||
addIfNotNull(textDraft)
|
||||
addIfNotNull(mentionsDraft)
|
||||
addIfNotNull(bodyRangesDraft)
|
||||
addIfNotNull(quoteDraft)
|
||||
addIfNotNull(locationDraft)
|
||||
addIfNotNull(voiceNoteDraft)
|
||||
|
@ -38,7 +38,7 @@ data class DraftState(
|
|||
return copy(
|
||||
threadId = threadId,
|
||||
textDraft = drafts.getDraftOfType(DraftTable.Draft.TEXT),
|
||||
mentionsDraft = drafts.getDraftOfType(DraftTable.Draft.MENTION),
|
||||
bodyRangesDraft = drafts.getDraftOfType(DraftTable.Draft.BODY_RANGES),
|
||||
quoteDraft = drafts.getDraftOfType(DraftTable.Draft.QUOTE),
|
||||
locationDraft = drafts.getDraftOfType(DraftTable.Draft.LOCATION),
|
||||
voiceNoteDraft = drafts.getDraftOfType(DraftTable.Draft.VOICE_NOTE),
|
||||
|
|
|
@ -64,9 +64,19 @@ class DraftViewModel @JvmOverloads constructor(
|
|||
store.update { it.copy(recipientId = recipient.id) }
|
||||
}
|
||||
|
||||
fun setTextDraft(text: String, mentions: List<Mention>) {
|
||||
fun setTextDraft(text: String, mentions: List<Mention>, styleBodyRanges: BodyRangeList?) {
|
||||
store.update {
|
||||
saveDrafts(it.copy(textDraft = text.toTextDraft(), mentionsDraft = mentions.toMentionsDraft()))
|
||||
val mentionRanges: BodyRangeList? = MentionUtil.mentionsToBodyRangeList(mentions)
|
||||
|
||||
val bodyRanges: BodyRangeList? = if (styleBodyRanges == null) {
|
||||
mentionRanges
|
||||
} else if (mentionRanges == null) {
|
||||
styleBodyRanges
|
||||
} else {
|
||||
styleBodyRanges.toBuilder().addAllRanges(mentionRanges.rangesList).build()
|
||||
}
|
||||
|
||||
saveDrafts(it.copy(textDraft = text.toTextDraft(), bodyRangesDraft = bodyRanges?.toDraft()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,11 +128,6 @@ private fun String.toTextDraft(): Draft? {
|
|||
return if (isNotEmpty()) Draft(Draft.TEXT, this) else null
|
||||
}
|
||||
|
||||
private fun List<Mention>.toMentionsDraft(): Draft? {
|
||||
val mentions: BodyRangeList? = MentionUtil.mentionsToBodyRangeList(this)
|
||||
return if (mentions != null) {
|
||||
Draft(Draft.MENTION, Base64.encodeBytes(mentions.toByteArray()))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
private fun BodyRangeList.toDraft(): Draft {
|
||||
return Draft(Draft.BODY_RANGES, Base64.encodeBytes(toByteArray()))
|
||||
}
|
||||
|
|
|
@ -116,6 +116,7 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor(
|
|||
.withMentions(conversationMessage.mentions)
|
||||
.withTimestamp(conversationMessage.messageRecord.timestamp)
|
||||
.withExpiration(conversationMessage.messageRecord.expireStarted + conversationMessage.messageRecord.expiresIn)
|
||||
.withBodyRanges(conversationMessage.messageRecord.messageRanges)
|
||||
|
||||
if (conversationMessage.multiselectCollection.isTextSelected(selectedParts)) {
|
||||
val mediaMessage: MmsMessageRecord? = conversationMessage.messageRecord as? MmsMessageRecord
|
||||
|
|
|
@ -99,7 +99,7 @@ class MessageQuotesRepository {
|
|||
.buildUpdatedModels(ApplicationDependencies.getApplication(), listOf(originalRecord))
|
||||
.get(0)
|
||||
|
||||
val originalMessage: ConversationMessage = ConversationMessageFactory.createWithUnresolvedData(application, originalRecord, originalRecord.getDisplayBody(application), false)
|
||||
val originalMessage: ConversationMessage = ConversationMessageFactory.createWithUnresolvedData(application, originalRecord, false)
|
||||
|
||||
return replies + originalMessage
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ import android.graphics.Typeface;
|
|||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.TouchDelegate;
|
||||
|
@ -58,6 +60,7 @@ import org.thoughtcrime.securesms.components.DeliveryStatusView;
|
|||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.components.TypingIndicatorView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiStrings;
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.ConversationSet;
|
||||
import org.thoughtcrime.securesms.database.MessageTypes;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable;
|
||||
|
@ -121,8 +124,9 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
|||
private int thumbSize;
|
||||
private GlideLiveDataTarget thumbTarget;
|
||||
|
||||
private int unreadCount;
|
||||
private AvatarImageView contactPhotoImage;
|
||||
private int unreadCount;
|
||||
private AvatarImageView contactPhotoImage;
|
||||
private SearchUtil.StyleFactory searchStyleFactory;
|
||||
|
||||
private LiveData<SpannableString> displayBody;
|
||||
|
||||
|
@ -153,6 +157,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
|||
this.unreadMentions = findViewById(R.id.conversation_list_item_unread_mentions_indicator);
|
||||
this.thumbSize = (int) DimensionUnit.SP.toPixels(16f);
|
||||
this.thumbTarget = new GlideLiveDataTarget(thumbSize, thumbSize);
|
||||
this.searchStyleFactory = () -> new CharacterStyle[] { new ForegroundColorSpan(ContextCompat.getColor(getContext(), R.color.signal_colorOnSurface)), SpanUtil.getBoldSpan() };
|
||||
|
||||
getLayoutTransition().setDuration(150);
|
||||
}
|
||||
|
@ -218,7 +223,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
|||
if (highlightSubstring != null) {
|
||||
String name = recipient.get().isSelf() ? getContext().getString(R.string.note_to_self) : recipient.get().getDisplayName(getContext());
|
||||
|
||||
this.fromView.setText(recipient.get(), SearchUtil.getHighlightedSpan(locale, SpanUtil::getMediumBoldSpan, name, highlightSubstring, SearchUtil.MATCH_ALL), true, null);
|
||||
this.fromView.setText(recipient.get(), SearchUtil.getHighlightedSpan(locale, searchStyleFactory, name, highlightSubstring, SearchUtil.MATCH_ALL), true, null);
|
||||
} else {
|
||||
this.fromView.setText(recipient.get(), false);
|
||||
}
|
||||
|
@ -274,8 +279,8 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
|||
observeDisplayBody(null, null);
|
||||
setSubjectViewText(null);
|
||||
|
||||
fromView.setText(contact, SearchUtil.getHighlightedSpan(locale, SpanUtil::getMediumBoldSpan, new SpannableString(contact.getDisplayName(getContext())), highlightSubstring, SearchUtil.MATCH_ALL), true, null);
|
||||
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, SpanUtil::getBoldSpan, contact.getE164().orElse(""), highlightSubstring, SearchUtil.MATCH_ALL));
|
||||
fromView.setText(contact, SearchUtil.getHighlightedSpan(locale, searchStyleFactory, new SpannableString(contact.getDisplayName(getContext())), highlightSubstring, SearchUtil.MATCH_ALL), true, null);
|
||||
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, searchStyleFactory, contact.getE164().orElse(""), highlightSubstring, SearchUtil.MATCH_ALL));
|
||||
dateView.setText("");
|
||||
archivedView.setVisibility(GONE);
|
||||
unreadIndicator.setVisibility(GONE);
|
||||
|
@ -303,7 +308,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
|||
setSubjectViewText(null);
|
||||
|
||||
fromView.setText(recipient.get(), false);
|
||||
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, SpanUtil::getBoldSpan, messageResult.getBodySnippet(), highlightSubstring, SearchUtil.MATCH_ALL));
|
||||
setSubjectViewText(SearchUtil.getHighlightedSpan(locale, searchStyleFactory, messageResult.getBodySnippet(), highlightSubstring, SearchUtil.MATCH_ALL));
|
||||
dateView.setText(DateUtils.getBriefRelativeTimeSpanString(getContext(), locale, messageResult.getReceivedTimestampMs()));
|
||||
archivedView.setVisibility(GONE);
|
||||
unreadIndicator.setVisibility(GONE);
|
||||
|
@ -491,7 +496,7 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
|||
|
||||
if (highlightSubstring != null) {
|
||||
String name = recipient.isSelf() ? getContext().getString(R.string.note_to_self) : recipient.getDisplayName(getContext());
|
||||
fromView.setText(recipient, SearchUtil.getHighlightedSpan(locale, SpanUtil::getMediumBoldSpan, new SpannableString(name), highlightSubstring, SearchUtil.MATCH_ALL), true, null);
|
||||
fromView.setText(recipient, SearchUtil.getHighlightedSpan(locale, searchStyleFactory, new SpannableString(name), highlightSubstring, SearchUtil.MATCH_ALL), true, null);
|
||||
} else {
|
||||
fromView.setText(recipient, false);
|
||||
}
|
||||
|
@ -580,7 +585,10 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
|||
} else if (extra != null && extra.isRemoteDelete()) {
|
||||
return emphasisAdded(context, context.getString(thread.isOutgoing() ? R.string.ThreadRecord_you_deleted_this_message : R.string.ThreadRecord_this_message_was_deleted), defaultTint);
|
||||
} else {
|
||||
String body = removeNewlines(thread.getBody());
|
||||
SpannableStringBuilder sourceBody = new SpannableStringBuilder(thread.getBody());
|
||||
MessageStyler.style(thread.getBodyRanges(), sourceBody);
|
||||
|
||||
CharSequence body = StringUtil.replace(sourceBody, '\n', " ");
|
||||
LiveData<SpannableString> finalBody = Transformations.map(createFinalBodyWithMediaIcon(context, body, thread, glideRequests, thumbSize, thumbTarget), updatedBody -> {
|
||||
if (thread.getRecipient().isGroup()) {
|
||||
RecipientId groupMessageSender = thread.getGroupMessageSender();
|
||||
|
@ -592,13 +600,13 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
|||
return new SpannableString(updatedBody);
|
||||
});
|
||||
|
||||
return whileLoadingShow(body, finalBody);
|
||||
return whileLoadingShow(sourceBody, finalBody);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static LiveData<CharSequence> createFinalBodyWithMediaIcon(@NonNull Context context,
|
||||
@NonNull String body,
|
||||
@NonNull CharSequence body,
|
||||
@NonNull ThreadRecord thread,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
@Px int thumbSize,
|
||||
|
@ -608,16 +616,16 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
|||
return LiveDataUtil.just(body);
|
||||
}
|
||||
|
||||
final String bodyWithoutMediaPrefix;
|
||||
final SpannableStringBuilder bodyWithoutMediaPrefix = SpannableStringBuilder.valueOf(body);
|
||||
|
||||
if (body.startsWith(EmojiStrings.GIF)) {
|
||||
bodyWithoutMediaPrefix = body.replaceFirst(EmojiStrings.GIF, "");
|
||||
} else if (body.startsWith(EmojiStrings.VIDEO)) {
|
||||
bodyWithoutMediaPrefix = body.replaceFirst(EmojiStrings.VIDEO, "");
|
||||
} else if (body.startsWith(EmojiStrings.PHOTO)) {
|
||||
bodyWithoutMediaPrefix = body.replaceFirst(EmojiStrings.PHOTO, "");
|
||||
} else if (thread.getExtra() != null && thread.getExtra().getStickerEmoji() != null && body.startsWith(thread.getExtra().getStickerEmoji())) {
|
||||
bodyWithoutMediaPrefix = body.replaceFirst(thread.getExtra().getStickerEmoji(), "");
|
||||
if (StringUtil.startsWith(body, EmojiStrings.GIF)) {
|
||||
bodyWithoutMediaPrefix.replace(0, EmojiStrings.GIF.length(), "");
|
||||
} else if (StringUtil.startsWith(body, EmojiStrings.VIDEO)) {
|
||||
bodyWithoutMediaPrefix.replace(0, EmojiStrings.VIDEO.length(), "");
|
||||
} else if (StringUtil.startsWith(body, EmojiStrings.PHOTO)) {
|
||||
bodyWithoutMediaPrefix.replace(0, EmojiStrings.PHOTO.length(), "");
|
||||
} else if (thread.getExtra() != null && thread.getExtra().getStickerEmoji() != null && StringUtil.startsWith(body, thread.getExtra().getStickerEmoji())) {
|
||||
bodyWithoutMediaPrefix.replace(0, thread.getExtra().getStickerEmoji().length(), "");
|
||||
} else {
|
||||
return LiveDataUtil.just(body);
|
||||
}
|
||||
|
@ -668,20 +676,8 @@ public final class ConversationListItem extends ConstraintLayout implements Bind
|
|||
/**
|
||||
* After a short delay, if the main data hasn't shown yet, then a loading message is displayed.
|
||||
*/
|
||||
private static @NonNull LiveData<SpannableString> whileLoadingShow(@NonNull String loading, @NonNull LiveData<SpannableString> string) {
|
||||
return LiveDataUtil.until(string, LiveDataUtil.delay(250, new SpannableString(loading)));
|
||||
}
|
||||
|
||||
private static @NonNull String removeNewlines(@Nullable String text) {
|
||||
if (text == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (text.indexOf('\n') >= 0) {
|
||||
return text.replaceAll("\n", " ");
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
private static @NonNull LiveData<SpannableString> whileLoadingShow(@NonNull CharSequence loading, @NonNull LiveData<SpannableString> string) {
|
||||
return LiveDataUtil.until(string, LiveDataUtil.delay(250, SpannableString.valueOf(loading)));
|
||||
}
|
||||
|
||||
private static @NonNull LiveData<SpannableString> emphasisAdded(@NonNull Context context, @NonNull String string, @ColorInt int defaultTint) {
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
package org.thoughtcrime.securesms.database
|
||||
|
||||
/**
|
||||
* Store data about an operation that changes the contents of a body.
|
||||
*/
|
||||
data class BodyAdjustment(val startIndex: Int, val oldLength: Int, val newLength: Int)
|
|
@ -0,0 +1,37 @@
|
|||
@file:JvmName("BodyRangeUtil")
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
|
||||
/**
|
||||
* Given a list of body adjustment from removing mention names from a message and replacing
|
||||
* them with a placeholder, we need to adjust the ranges within [BodyRangeList] to account
|
||||
* for the now shorter text.
|
||||
*/
|
||||
fun BodyRangeList?.adjustBodyRanges(bodyAdjustments: List<BodyAdjustment>): BodyRangeList? {
|
||||
if (this == null || bodyAdjustments.isEmpty()) {
|
||||
return this
|
||||
}
|
||||
|
||||
val newBodyRanges = rangesList.toMutableList()
|
||||
|
||||
for (adjustment in bodyAdjustments) {
|
||||
val adjustmentLength = adjustment.oldLength - adjustment.newLength
|
||||
|
||||
rangesList.forEachIndexed { listIndex, range ->
|
||||
val needsRangeStartsAfterAdjustment = range.start > adjustment.startIndex
|
||||
val needsRangeCoversAdjustment = range.start <= adjustment.startIndex && range.start + range.length >= adjustment.startIndex + adjustment.oldLength
|
||||
|
||||
val newRange = newBodyRanges[listIndex]
|
||||
val newStart: Int? = if (needsRangeStartsAfterAdjustment) newRange.start - adjustmentLength else null
|
||||
val newLength: Int? = if (needsRangeCoversAdjustment) newRange.length - adjustmentLength else null
|
||||
|
||||
if (newStart != null || newLength != null) {
|
||||
newBodyRanges[listIndex] = newRange.toBuilder().setStart(newStart ?: newRange.start).setLength(newLength ?: newRange.length).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return BodyRangeList.newBuilder().addAllRanges(newBodyRanges).build()
|
||||
}
|
|
@ -139,7 +139,7 @@ class DraftTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
|||
const val AUDIO = "audio"
|
||||
const val LOCATION = "location"
|
||||
const val QUOTE = "quote"
|
||||
const val MENTION = "mention"
|
||||
const val BODY_RANGES = "mention"
|
||||
const val VOICE_NOTE = "voice_note"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,10 +10,7 @@ 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.RecipientTable.MentionSetting;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
|
@ -35,25 +32,14 @@ public final class MentionUtil {
|
|||
private MentionUtil() { }
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull CharSequence updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord) {
|
||||
return updateBodyWithDisplayNames(context, messageRecord, messageRecord.getDisplayBody(context));
|
||||
public static @Nullable CharSequence updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord) {
|
||||
return updateBodyWithDisplayNames(context, messageRecord, messageRecord.getDisplayBody(context)).getBody();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull CharSequence updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body) {
|
||||
if (messageRecord.isMms()) {
|
||||
List<Mention> mentions = SignalDatabase.mentions().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);
|
||||
public static @NonNull UpdatedBodyAndMentions updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body) {
|
||||
List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId());
|
||||
return updateBodyAndMentionsWithDisplayNames(context, body, mentions);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -68,12 +54,13 @@ public final class MentionUtil {
|
|||
@VisibleForTesting
|
||||
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);
|
||||
return new UpdatedBodyAndMentions(body, mentions, Collections.emptyList());
|
||||
}
|
||||
|
||||
SortedSet<Mention> sortedMentions = new TreeSet<>(mentions);
|
||||
SpannableStringBuilder updatedBody = new SpannableStringBuilder();
|
||||
List<Mention> updatedMentions = new ArrayList<>();
|
||||
List<BodyAdjustment> bodyAdjustments = new ArrayList<>();
|
||||
|
||||
int bodyIndex = 0;
|
||||
|
||||
|
@ -89,6 +76,8 @@ public final class MentionUtil {
|
|||
updatedBody.append(replaceWith);
|
||||
updatedMentions.add(updatedMention);
|
||||
|
||||
bodyAdjustments.add(new BodyAdjustment(mention.getStart(), mention.getLength(), updatedMention.getLength()));
|
||||
|
||||
bodyIndex = mention.getStart() + mention.getLength();
|
||||
}
|
||||
|
||||
|
@ -96,7 +85,7 @@ public final class MentionUtil {
|
|||
updatedBody.append(body.subSequence(bodyIndex, body.length()));
|
||||
}
|
||||
|
||||
return new UpdatedBodyAndMentions(updatedBody.toString(), updatedMentions);
|
||||
return new UpdatedBodyAndMentions(updatedBody, updatedMentions, bodyAdjustments);
|
||||
}
|
||||
|
||||
public static @Nullable BodyRangeList mentionsToBodyRangeList(@Nullable List<Mention> mentions) {
|
||||
|
@ -117,34 +106,20 @@ public final class MentionUtil {
|
|||
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(ServiceId.parseOrThrow(mention.getMentionUuid())).getId();
|
||||
return new Mention(id, mention.getStart(), mention.getLength());
|
||||
})
|
||||
.toList();
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
public static @NonNull List<Mention> bodyRangeListToMentions(@Nullable BodyRangeList bodyRanges) {
|
||||
if (bodyRanges != null) {
|
||||
return Stream.of(bodyRanges.getRangesList())
|
||||
.filter(bodyRange -> bodyRange.getAssociatedValueCase() == BodyRangeList.BodyRange.AssociatedValueCase.MENTIONUUID)
|
||||
.map(mention -> {
|
||||
RecipientId id = Recipient.externalPush(ServiceId.parseOrThrow(mention.getMentionUuid())).getId();
|
||||
return new Mention(id, mention.getStart(), mention.getLength());
|
||||
})
|
||||
.toList();
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull String getMentionSettingDisplayValue(@NonNull Context context, @NonNull MentionSetting mentionSetting) {
|
||||
switch (mentionSetting) {
|
||||
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);
|
||||
}
|
||||
|
||||
private static boolean invalidMention(@NonNull CharSequence body, @NonNull Mention mention) {
|
||||
int start = mention.getStart();
|
||||
int length = mention.getLength();
|
||||
|
@ -153,12 +128,14 @@ public final class MentionUtil {
|
|||
}
|
||||
|
||||
public static class UpdatedBodyAndMentions {
|
||||
@Nullable private final CharSequence body;
|
||||
@NonNull private final List<Mention> mentions;
|
||||
@Nullable private final CharSequence body;
|
||||
@NonNull private final List<Mention> mentions;
|
||||
@NonNull private final List<BodyAdjustment> bodyAdjustments;
|
||||
|
||||
public UpdatedBodyAndMentions(@Nullable CharSequence body, @NonNull List<Mention> mentions) {
|
||||
this.body = body;
|
||||
this.mentions = mentions;
|
||||
private UpdatedBodyAndMentions(@Nullable CharSequence body, @NonNull List<Mention> mentions, @NonNull List<BodyAdjustment> bodyAdjustments) {
|
||||
this.body = body;
|
||||
this.mentions = mentions;
|
||||
this.bodyAdjustments = bodyAdjustments;
|
||||
}
|
||||
|
||||
public @Nullable CharSequence getBody() {
|
||||
|
@ -169,6 +146,10 @@ public final class MentionUtil {
|
|||
return mentions;
|
||||
}
|
||||
|
||||
public @NonNull List<BodyAdjustment> getBodyAdjustments() {
|
||||
return bodyAdjustments;
|
||||
}
|
||||
|
||||
@Nullable String getBodyAsString() {
|
||||
return body != null ? body.toString() : null;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.thoughtcrime.securesms.database;
|
|||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -47,6 +48,7 @@ import org.thoughtcrime.securesms.attachments.AttachmentId;
|
|||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||
import org.thoughtcrime.securesms.database.documents.Document;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet;
|
||||
|
@ -169,7 +171,7 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
|||
public static final String QUOTE_AUTHOR = "quote_author";
|
||||
public static final String QUOTE_BODY = "quote_body";
|
||||
public static final String QUOTE_MISSING = "quote_missing";
|
||||
public static final String QUOTE_MENTIONS = "quote_mentions";
|
||||
public static final String QUOTE_BODY_RANGES = "quote_mentions";
|
||||
public static final String QUOTE_TYPE = "quote_type";
|
||||
public static final String SHARED_CONTACTS = "shared_contacts";
|
||||
public static final String LINK_PREVIEWS = "link_previews";
|
||||
|
@ -216,7 +218,7 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
|||
QUOTE_AUTHOR + " INTEGER DEFAULT 0, " +
|
||||
QUOTE_BODY + " TEXT DEFAULT NULL, " +
|
||||
QUOTE_MISSING + " INTEGER DEFAULT 0, " +
|
||||
QUOTE_MENTIONS + " BLOB DEFAULT NULL," +
|
||||
QUOTE_BODY_RANGES + " BLOB DEFAULT NULL," +
|
||||
QUOTE_TYPE + " INTEGER DEFAULT 0," +
|
||||
SHARED_CONTACTS + " TEXT DEFAULT NULL, " +
|
||||
UNIDENTIFIED + " INTEGER DEFAULT 0, " +
|
||||
|
@ -281,7 +283,7 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
|||
QUOTE_BODY,
|
||||
QUOTE_TYPE,
|
||||
QUOTE_MISSING,
|
||||
QUOTE_MENTIONS,
|
||||
QUOTE_BODY_RANGES,
|
||||
SHARED_CONTACTS,
|
||||
LINK_PREVIEWS,
|
||||
UNIDENTIFIED,
|
||||
|
@ -2188,6 +2190,7 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
|||
String networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(MessageTable.NETWORK_FAILURES));
|
||||
StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE));
|
||||
ParentStoryId parentStoryId = ParentStoryId.deserialize(CursorUtil.requireLong(cursor, PARENT_STORY_ID));
|
||||
byte[] messageRangesData = CursorUtil.requireBlob(cursor, MESSAGE_RANGES);
|
||||
|
||||
long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID));
|
||||
long quoteAuthor = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR));
|
||||
|
@ -2195,7 +2198,8 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
|||
int quoteType = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE_TYPE));
|
||||
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(context, cursor);
|
||||
List<Mention> quoteMentions = parseQuoteMentions(cursor);
|
||||
BodyRangeList quoteBodyRanges = parseQuoteBodyRanges(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);
|
||||
|
@ -2212,7 +2216,7 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
|||
QuoteModel quote = null;
|
||||
|
||||
if (quoteId > 0 && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || !quoteAttachments.isEmpty())) {
|
||||
quote = new QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteAttachments, quoteMentions, QuoteModel.Type.fromCode(quoteType));
|
||||
quote = new QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteAttachments, quoteMentions, QuoteModel.Type.fromCode(quoteType), quoteBodyRanges);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(mismatchDocument)) {
|
||||
|
@ -2248,6 +2252,15 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
|||
giftBadge = GiftBadge.parseFrom(Base64.decode(body));
|
||||
}
|
||||
|
||||
BodyRangeList messageRanges = null;
|
||||
if (messageRangesData != null) {
|
||||
try {
|
||||
messageRanges = BodyRangeList.parseFrom(messageRangesData);
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "Error parsing message ranges", e);
|
||||
}
|
||||
}
|
||||
|
||||
OutgoingMessage message = new OutgoingMessage(recipient,
|
||||
body,
|
||||
attachments,
|
||||
|
@ -2266,7 +2279,8 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
|||
networkFailures,
|
||||
mismatches,
|
||||
giftBadge,
|
||||
MessageTypes.isSecureType(outboxType));
|
||||
MessageTypes.isSecureType(outboxType),
|
||||
messageRanges);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
@ -2398,14 +2412,21 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
|||
|
||||
if (retrieved.getQuote() != null) {
|
||||
contentValues.put(QUOTE_ID, retrieved.getQuote().getId());
|
||||
contentValues.put(QUOTE_BODY, retrieved.getQuote().getText().toString());
|
||||
contentValues.put(QUOTE_BODY, retrieved.getQuote().getText());
|
||||
contentValues.put(QUOTE_AUTHOR, retrieved.getQuote().getAuthor().serialize());
|
||||
contentValues.put(QUOTE_TYPE, retrieved.getQuote().getType().getCode());
|
||||
contentValues.put(QUOTE_MISSING, retrieved.getQuote().isOriginalMissing() ? 1 : 0);
|
||||
|
||||
BodyRangeList.Builder quoteBodyRanges = retrieved.getQuote().getBodyRanges() != null ? retrieved.getQuote().getBodyRanges().toBuilder()
|
||||
: BodyRangeList.newBuilder();
|
||||
|
||||
BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(retrieved.getQuote().getMentions());
|
||||
if (mentionsList != null) {
|
||||
contentValues.put(QUOTE_MENTIONS, mentionsList.toByteArray());
|
||||
quoteBodyRanges.addAllRanges(mentionsList.getRangesList());
|
||||
}
|
||||
|
||||
if (quoteBodyRanges.getRangesCount() > 0) {
|
||||
contentValues.put(QUOTE_BODY_RANGES, quoteBodyRanges.build().toByteArray());
|
||||
}
|
||||
|
||||
quoteAttachments = retrieved.getQuote().getAttachments();
|
||||
|
@ -2789,17 +2810,30 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
|||
contentValues.put(QUOTE_TYPE, message.getOutgoingQuote().getType().getCode());
|
||||
contentValues.put(QUOTE_MISSING, message.getOutgoingQuote().isOriginalMissing() ? 1 : 0);
|
||||
|
||||
BodyRangeList adjustedQuoteBodyRanges = BodyRangeUtil.adjustBodyRanges(message.getOutgoingQuote().getBodyRanges(), updated.getBodyAdjustments());
|
||||
BodyRangeList.Builder quoteBodyRanges;
|
||||
if (adjustedQuoteBodyRanges != null) {
|
||||
quoteBodyRanges = adjustedQuoteBodyRanges.toBuilder();
|
||||
} else {
|
||||
quoteBodyRanges = BodyRangeList.newBuilder();
|
||||
}
|
||||
|
||||
BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(updated.getMentions());
|
||||
if (mentionsList != null) {
|
||||
contentValues.put(QUOTE_MENTIONS, mentionsList.toByteArray());
|
||||
quoteBodyRanges.addAllRanges(mentionsList.getRangesList());
|
||||
}
|
||||
|
||||
if (quoteBodyRanges.getRangesCount() > 0) {
|
||||
contentValues.put(QUOTE_BODY_RANGES, quoteBodyRanges.build().toByteArray());
|
||||
}
|
||||
|
||||
quoteAttachments.addAll(message.getOutgoingQuote().getAttachments());
|
||||
}
|
||||
|
||||
MentionUtil.UpdatedBodyAndMentions updatedBodyAndMentions = MentionUtil.updateBodyAndMentionsWithPlaceholders(message.getBody(), message.getMentions());
|
||||
BodyRangeList bodyRanges = BodyRangeUtil.adjustBodyRanges(message.getBodyRanges(), updatedBodyAndMentions.getBodyAdjustments());
|
||||
|
||||
long messageId = insertMediaMessage(threadId, updatedBodyAndMentions.getBodyAsString(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), updatedBodyAndMentions.getMentions(), null, contentValues, insertListener, false, false);
|
||||
long messageId = insertMediaMessage(threadId, updatedBodyAndMentions.getBodyAsString(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), updatedBodyAndMentions.getMentions(), bodyRanges, contentValues, insertListener, false, false);
|
||||
|
||||
if (message.getRecipient().isGroup()) {
|
||||
GroupReceiptTable receiptDatabase = SignalDatabase.groupReceipts();
|
||||
|
@ -3281,10 +3315,37 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
|||
}
|
||||
}
|
||||
|
||||
private static @NonNull List<Mention> parseQuoteMentions(@NonNull Context context, Cursor cursor) {
|
||||
byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(QUOTE_MENTIONS));
|
||||
private static @NonNull List<Mention> parseQuoteMentions(@NonNull Cursor cursor) {
|
||||
byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(QUOTE_BODY_RANGES));
|
||||
BodyRangeList bodyRanges = null;
|
||||
|
||||
return MentionUtil.bodyRangeListToMentions(context, raw);
|
||||
if (raw != null) {
|
||||
try {
|
||||
bodyRanges = BodyRangeList.parseFrom(raw);
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "Unable to parse quote body ranges", e);
|
||||
}
|
||||
}
|
||||
|
||||
return MentionUtil.bodyRangeListToMentions(bodyRanges);
|
||||
}
|
||||
|
||||
private static @Nullable BodyRangeList parseQuoteBodyRanges(@NonNull Cursor cursor) {
|
||||
byte[] data = cursor.getBlob(cursor.getColumnIndexOrThrow(QUOTE_BODY_RANGES));
|
||||
|
||||
if (data != null) {
|
||||
try {
|
||||
final List<BodyRangeList.BodyRange> bodyRanges = Stream.of(BodyRangeList.parseFrom(data).getRangesList())
|
||||
.filter(bodyRange -> bodyRange.getAssociatedValueCase() != BodyRangeList.BodyRange.AssociatedValueCase.MENTIONUUID)
|
||||
.toList();
|
||||
|
||||
return BodyRangeList.newBuilder().addAllRanges(bodyRanges).build();
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
// Intentionally left blank
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public SQLiteDatabase beginTransaction() {
|
||||
|
@ -4574,6 +4635,32 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
|||
}
|
||||
}
|
||||
|
||||
public @NonNull Map<Long, BodyRangeList> getBodyRangesForMessages(@NonNull List<Long> messageIds) {
|
||||
List<SqlUtil.Query> queries = SqlUtil.buildCollectionQuery(ID, messageIds);
|
||||
Map<Long, BodyRangeList> bodyRanges = new HashMap<>();
|
||||
|
||||
for (SqlUtil.Query query : queries) {
|
||||
try (Cursor cursor = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), ID, MESSAGE_RANGES)
|
||||
.from(TABLE_NAME)
|
||||
.where(query.getWhere(), query.getWhereArgs())
|
||||
.run())
|
||||
{
|
||||
while (cursor.moveToNext()) {
|
||||
byte[] data = CursorUtil.requireBlob(cursor, MESSAGE_RANGES);
|
||||
if (data != null) {
|
||||
try {
|
||||
bodyRanges.put(CursorUtil.requireLong(cursor, ID), BodyRangeList.parseFrom(data));
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "Unable to parse body ranges for search", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bodyRanges;
|
||||
}
|
||||
|
||||
protected enum ReceiptType {
|
||||
READ(READ_RECEIPT_COUNT, GroupReceiptTable.STATUS_READ),
|
||||
DELIVERY(DELIVERY_RECEIPT_COUNT, GroupReceiptTable.STATUS_DELIVERED),
|
||||
|
@ -4922,13 +5009,17 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
|||
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();
|
||||
CharSequence quoteText = message.getOutgoingQuote() != null ? message.getOutgoingQuote().getText() : null;
|
||||
List<Mention> quoteMentions = message.getOutgoingQuote() != null ? message.getOutgoingQuote().getMentions() : Collections.emptyList();
|
||||
BodyRangeList quoteBodyRanges = message.getOutgoingQuote() != null ? message.getOutgoingQuote().getBodyRanges() : null;
|
||||
|
||||
if (quoteText != null && !quoteMentions.isEmpty()) {
|
||||
if (quoteText != null && (Util.hasItems(quoteMentions) || quoteBodyRanges != null)) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions);
|
||||
|
||||
quoteText = updated.getBody();
|
||||
SpannableString styledText = new SpannableString(updated.getBody());
|
||||
MessageStyler.style(BodyRangeUtil.adjustBodyRanges(quoteBodyRanges, updated.getBodyAdjustments()), styledText);
|
||||
|
||||
quoteText = styledText;
|
||||
quoteMentions = updated.getMentions();
|
||||
}
|
||||
|
||||
|
@ -5202,16 +5293,20 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
|||
CharSequence quoteText = cursor.getString(cursor.getColumnIndexOrThrow(MessageTable.QUOTE_BODY));
|
||||
int quoteType = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.QUOTE_TYPE));
|
||||
boolean quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.QUOTE_MISSING)) == 1;
|
||||
List<Mention> quoteMentions = parseQuoteMentions(context, cursor);
|
||||
List<Mention> quoteMentions = parseQuoteMentions(cursor);
|
||||
BodyRangeList bodyRanges = parseQuoteBodyRanges(cursor);
|
||||
List<DatabaseAttachment> attachments = SignalDatabase.attachments().getAttachments(cursor);
|
||||
List<? extends Attachment> quoteAttachments = Stream.of(attachments).filter(Attachment::isQuote).toList();
|
||||
SlideDeck quoteDeck = new SlideDeck(context, quoteAttachments);
|
||||
|
||||
if (quoteId > 0 && quoteAuthor > 0) {
|
||||
if (quoteText != null && !quoteMentions.isEmpty()) {
|
||||
if (quoteText != null && (Util.hasItems(quoteMentions) || bodyRanges != null)) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions);
|
||||
|
||||
quoteText = updated.getBody();
|
||||
SpannableString styledText = new SpannableString(updated.getBody());
|
||||
MessageStyler.style(BodyRangeUtil.adjustBodyRanges(bodyRanges, updated.getBodyAdjustments()), styledText);
|
||||
|
||||
quoteText = styledText;
|
||||
quoteMentions = updated.getMentions();
|
||||
}
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@ import org.thoughtcrime.securesms.mms.StickerSlide;
|
|||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ThreadBodyUtil {
|
||||
|
@ -28,19 +30,19 @@ public final class ThreadBodyUtil {
|
|||
private ThreadBodyUtil() {
|
||||
}
|
||||
|
||||
public static @NonNull String getFormattedBodyFor(@NonNull Context context, @NonNull MessageRecord record) {
|
||||
public static @NonNull ThreadBody getFormattedBodyFor(@NonNull Context context, @NonNull MessageRecord record) {
|
||||
if (record.isMms()) {
|
||||
return getFormattedBodyForMms(context, (MmsMessageRecord) record);
|
||||
}
|
||||
|
||||
return record.getBody();
|
||||
return new ThreadBody(record.getBody());
|
||||
}
|
||||
|
||||
private static @NonNull String getFormattedBodyForMms(@NonNull Context context, @NonNull MmsMessageRecord record) {
|
||||
private static @NonNull ThreadBody getFormattedBodyForMms(@NonNull Context context, @NonNull MmsMessageRecord record) {
|
||||
if (record.getSharedContacts().size() > 0) {
|
||||
Contact contact = record.getSharedContacts().get(0);
|
||||
|
||||
return ContactUtil.getStringSummary(context, contact).toString();
|
||||
return new ThreadBody(ContactUtil.getStringSummary(context, contact).toString());
|
||||
} else if (record.getSlideDeck().getDocumentSlide() != null) {
|
||||
return format(context, record, EmojiStrings.FILE, R.string.ThreadRecord_file);
|
||||
} else if (record.getSlideDeck().getAudioSlide() != null) {
|
||||
|
@ -49,17 +51,17 @@ public final class ThreadBodyUtil {
|
|||
String emoji = getStickerEmoji(record);
|
||||
return format(context, record, emoji, R.string.ThreadRecord_sticker);
|
||||
} else if (MessageRecordUtil.hasGiftBadge(record)) {
|
||||
return String.format("%s %s", EmojiStrings.GIFT, getGiftSummary(context, record));
|
||||
return format(EmojiStrings.GIFT, getGiftSummary(context, record));
|
||||
} else if (MessageRecordUtil.isStoryReaction(record)) {
|
||||
return getStoryReactionSummary(context, record);
|
||||
return new ThreadBody(getStoryReactionSummary(context, record));
|
||||
} else if (record.isPaymentNotification()) {
|
||||
return String.format("%s %s", EmojiStrings.CARD, context.getString(R.string.ThreadRecord_payment));
|
||||
return format(EmojiStrings.CARD, context.getString(R.string.ThreadRecord_payment));
|
||||
} else if (record.isPaymentsRequestToActivate()) {
|
||||
return String.format("%s %s", EmojiStrings.CARD, getPaymentActivationRequestSummary(context, record));
|
||||
return format(EmojiStrings.CARD, getPaymentActivationRequestSummary(context, record));
|
||||
} else if (record.isPaymentsActivated()) {
|
||||
return String.format("%s %s", EmojiStrings.CARD, getPaymentActivatedSummary(context, record));
|
||||
return format(EmojiStrings.CARD, getPaymentActivatedSummary(context, record));
|
||||
} else if (record.isCallLog() && !record.isGroupCall()) {
|
||||
return getCallLogSummary(context, record);
|
||||
return new ThreadBody(getCallLogSummary(context, record));
|
||||
}
|
||||
|
||||
boolean hasImage = false;
|
||||
|
@ -79,7 +81,7 @@ public final class ThreadBodyUtil {
|
|||
} else if (hasImage) {
|
||||
return format(context, record, EmojiStrings.PHOTO, R.string.ThreadRecord_photo);
|
||||
} else if (TextUtils.isEmpty(record.getBody())) {
|
||||
return context.getString(R.string.ThreadRecord_media_message);
|
||||
return new ThreadBody(context.getString(R.string.ThreadRecord_media_message));
|
||||
} else {
|
||||
return getBody(context, record);
|
||||
}
|
||||
|
@ -146,16 +148,23 @@ public final class ThreadBodyUtil {
|
|||
}
|
||||
}
|
||||
|
||||
private static @NonNull String format(@NonNull Context context, @NonNull MessageRecord record, @NonNull String emoji, @StringRes int defaultStringRes) {
|
||||
return String.format("%s %s", emoji, getBodyOrDefault(context, record, defaultStringRes));
|
||||
private static @NonNull ThreadBody format(@NonNull Context context, @NonNull MessageRecord record, @NonNull String emoji, @StringRes int defaultStringRes) {
|
||||
CharSequence body = getBodyOrDefault(context, record, defaultStringRes).getBody();
|
||||
return format(emoji, body);
|
||||
}
|
||||
|
||||
private static @NonNull String getBodyOrDefault(@NonNull Context context, @NonNull MessageRecord record, @StringRes int defaultStringRes) {
|
||||
return TextUtils.isEmpty(record.getBody()) ? context.getString(defaultStringRes) : getBody(context, record);
|
||||
private static @NonNull ThreadBody format(@NonNull CharSequence prefix, @NonNull CharSequence body) {
|
||||
return new ThreadBody(String.format("%s %s", prefix, body), prefix.length() + 1);
|
||||
}
|
||||
|
||||
private static @NonNull String getBody(@NonNull Context context, @NonNull MessageRecord record) {
|
||||
return MentionUtil.updateBodyWithDisplayNames(context, record, record.getBody()).toString();
|
||||
private static @NonNull ThreadBody getBodyOrDefault(@NonNull Context context, @NonNull MessageRecord record, @StringRes int defaultStringRes) {
|
||||
return TextUtils.isEmpty(record.getBody()) ? new ThreadBody(context.getString(defaultStringRes)) : getBody(context, record);
|
||||
}
|
||||
|
||||
private static @NonNull ThreadBody getBody(@NonNull Context context, @NonNull MessageRecord record) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyWithDisplayNames(context, record, record.getBody());
|
||||
//noinspection ConstantConditions
|
||||
return new ThreadBody(updated.getBody(), updated.getBodyAdjustments());
|
||||
}
|
||||
|
||||
private static @NonNull String getStickerEmoji(@NonNull MessageRecord record) {
|
||||
|
@ -164,4 +173,30 @@ public final class ThreadBodyUtil {
|
|||
return Util.isEmpty(slide.getEmoji()) ? EmojiStrings.STICKER
|
||||
: slide.getEmoji();
|
||||
}
|
||||
|
||||
public static class ThreadBody {
|
||||
private final CharSequence body;
|
||||
private final List<BodyAdjustment> bodyAdjustments;
|
||||
|
||||
public ThreadBody(@NonNull CharSequence body) {
|
||||
this(body, 0);
|
||||
}
|
||||
|
||||
public ThreadBody(@NonNull CharSequence body, int startOffset) {
|
||||
this(body, startOffset == 0 ? Collections.emptyList() : Collections.singletonList(new BodyAdjustment(0, 0, startOffset)));
|
||||
}
|
||||
|
||||
public ThreadBody(@NonNull CharSequence body, @NonNull List<BodyAdjustment> bodyAdjustments) {
|
||||
this.body = body;
|
||||
this.bodyAdjustments = bodyAdjustments;
|
||||
}
|
||||
|
||||
public @NonNull CharSequence getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public @NonNull List<BodyAdjustment> getBodyAdjustments() {
|
||||
return bodyAdjustments;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,10 +36,13 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.mentions
|
|||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messageLog
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messages
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
|
||||
import org.thoughtcrime.securesms.database.ThreadBodyUtil.ThreadBody
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.database.model.serialize
|
||||
import org.thoughtcrime.securesms.groups.BadGroupIdException
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
@ -1376,13 +1379,15 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
|||
}
|
||||
}
|
||||
|
||||
val threadBody: ThreadBody = ThreadBodyUtil.getFormattedBodyFor(context, record)
|
||||
|
||||
updateThread(
|
||||
threadId = threadId,
|
||||
meaningfulMessages = meaningfulMessages,
|
||||
body = ThreadBodyUtil.getFormattedBodyFor(context, record),
|
||||
body = threadBody.body.toString(),
|
||||
attachment = getAttachmentUriFor(record),
|
||||
contentType = getContentTypeFor(record),
|
||||
extra = getExtrasFor(record),
|
||||
extra = getExtrasFor(record, threadBody),
|
||||
date = record.timestamp,
|
||||
status = record.deliveryStatus,
|
||||
deliveryReceiptCount = record.deliveryReceiptCount,
|
||||
|
@ -1534,7 +1539,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
|||
return null
|
||||
}
|
||||
|
||||
private fun getExtrasFor(record: MessageRecord): Extra? {
|
||||
private fun getExtrasFor(record: MessageRecord, body: ThreadBody): Extra? {
|
||||
val threadRecipient = if (record.isOutgoing) record.recipient else getRecipientForThreadId(record.threadId)
|
||||
val messageRequestAccepted = RecipientUtil.isMessageRequestAccepted(record.threadId, threadRecipient)
|
||||
val individualRecipientId = record.individualRecipient.id
|
||||
|
@ -1567,7 +1572,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
|||
}
|
||||
}
|
||||
|
||||
return if (record.isRemoteDelete) {
|
||||
val extras: Extra? = if (record.isRemoteDelete) {
|
||||
Extra.forRemoteDelete(individualRecipientId)
|
||||
} else if (record.isViewOnce) {
|
||||
Extra.forViewOnce(individualRecipientId)
|
||||
|
@ -1581,6 +1586,13 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
|||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return if (record.messageRanges != null) {
|
||||
val bodyRanges = record.requireMessageRanges().adjustBodyRanges(body.bodyAdjustments)!!
|
||||
extras?.copy(bodyRanges = bodyRanges.serialize()) ?: Extra.forBodyRanges(bodyRanges, individualRecipientId)
|
||||
} else {
|
||||
extras
|
||||
}
|
||||
}
|
||||
|
||||
private fun createQuery(where: String, limit: Long): String {
|
||||
|
@ -1754,15 +1766,16 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
|||
}
|
||||
|
||||
data class Extra(
|
||||
@field:JsonProperty @param:JsonProperty("isRevealable") val isViewOnce: Boolean,
|
||||
@field:JsonProperty @param:JsonProperty("isSticker") val isSticker: Boolean,
|
||||
@field:JsonProperty @param:JsonProperty("stickerEmoji") val stickerEmoji: String?,
|
||||
@field:JsonProperty @param:JsonProperty("isAlbum") val isAlbum: Boolean,
|
||||
@field:JsonProperty @param:JsonProperty("isRemoteDelete") val isRemoteDelete: Boolean,
|
||||
@field:JsonProperty @param:JsonProperty("isMessageRequestAccepted") val isMessageRequestAccepted: Boolean,
|
||||
@field:JsonProperty @param:JsonProperty("isGv2Invite") val isGv2Invite: Boolean,
|
||||
@field:JsonProperty @param:JsonProperty("groupAddedBy") val groupAddedBy: String?,
|
||||
@field:JsonProperty @param:JsonProperty("individualRecipientId") private val individualRecipientId: String
|
||||
@field:JsonProperty @param:JsonProperty("isRevealable") val isViewOnce: Boolean = false,
|
||||
@field:JsonProperty @param:JsonProperty("isSticker") val isSticker: Boolean = false,
|
||||
@field:JsonProperty @param:JsonProperty("stickerEmoji") val stickerEmoji: String? = null,
|
||||
@field:JsonProperty @param:JsonProperty("isAlbum") val isAlbum: Boolean = false,
|
||||
@field:JsonProperty @param:JsonProperty("isRemoteDelete") val isRemoteDelete: Boolean = false,
|
||||
@field:JsonProperty @param:JsonProperty("isMessageRequestAccepted") val isMessageRequestAccepted: Boolean = true,
|
||||
@field:JsonProperty @param:JsonProperty("isGv2Invite") val isGv2Invite: Boolean = false,
|
||||
@field:JsonProperty @param:JsonProperty("groupAddedBy") val groupAddedBy: String? = null,
|
||||
@field:JsonProperty @param:JsonProperty("individualRecipientId") private val individualRecipientId: String,
|
||||
@field:JsonProperty @param:JsonProperty("bodyRanges") val bodyRanges: String? = null
|
||||
) {
|
||||
|
||||
fun getIndividualRecipientId(): String {
|
||||
|
@ -1771,35 +1784,39 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
|||
|
||||
companion object {
|
||||
fun forViewOnce(individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = true, isSticker = false, stickerEmoji = null, isAlbum = false, isRemoteDelete = false, isMessageRequestAccepted = true, isGv2Invite = false, groupAddedBy = null, individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(isViewOnce = true, individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forSticker(emoji: String?, individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = false, isSticker = true, stickerEmoji = emoji, isAlbum = false, isRemoteDelete = false, isMessageRequestAccepted = true, isGv2Invite = false, groupAddedBy = null, individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(isSticker = true, stickerEmoji = emoji, individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forAlbum(individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = false, isSticker = false, stickerEmoji = null, isAlbum = true, isRemoteDelete = false, isMessageRequestAccepted = true, isGv2Invite = false, groupAddedBy = null, individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(isAlbum = true, individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forRemoteDelete(individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = false, isSticker = false, stickerEmoji = null, isAlbum = false, isRemoteDelete = true, isMessageRequestAccepted = true, isGv2Invite = false, groupAddedBy = null, individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(isRemoteDelete = true, individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forMessageRequest(individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = false, isSticker = false, stickerEmoji = null, isAlbum = false, isRemoteDelete = false, isMessageRequestAccepted = false, isGv2Invite = false, groupAddedBy = null, individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(isMessageRequestAccepted = false, individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forGroupMessageRequest(recipientId: RecipientId, individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = false, isSticker = false, stickerEmoji = null, isAlbum = false, isRemoteDelete = false, isMessageRequestAccepted = false, isGv2Invite = false, groupAddedBy = recipientId.serialize(), individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(isMessageRequestAccepted = false, groupAddedBy = recipientId.serialize(), individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forGroupV2invite(recipientId: RecipientId, individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = false, isSticker = false, stickerEmoji = null, isAlbum = false, isRemoteDelete = false, isMessageRequestAccepted = false, isGv2Invite = true, groupAddedBy = recipientId.serialize(), individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(isGv2Invite = true, groupAddedBy = recipientId.serialize(), individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forDefault(individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = false, isSticker = false, stickerEmoji = null, isAlbum = false, isRemoteDelete = false, isMessageRequestAccepted = true, isGv2Invite = false, groupAddedBy = null, individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forBodyRanges(bodyRanges: BodyRangeList, individualRecipient: RecipientId): Extra {
|
||||
return Extra(individualRecipientId = individualRecipient.serialize(), bodyRanges = bodyRanges.serialize())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import org.signal.core.util.StringSerializer
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
|
||||
object BodyRangeListSerializer : StringSerializer<BodyRangeList> {
|
||||
override fun serialize(data: BodyRangeList): String = Base64.encodeBytes(data.toByteArray())
|
||||
override fun deserialize(data: String): BodyRangeList = BodyRangeList.parseFrom(Base64.decode(data))
|
||||
}
|
||||
|
||||
fun BodyRangeList.serialize(): String {
|
||||
return BodyRangeListSerializer.serialize(this)
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
@file:JvmName("DatabaseProtosUtil")
|
||||
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.BodyRange
|
||||
|
||||
/**
|
||||
* Collection of extensions to make working with database protos cleaner.
|
||||
|
@ -43,3 +47,28 @@ fun BodyRangeList.Builder.addButton(label: String, action: String, start: Int, l
|
|||
|
||||
return this
|
||||
}
|
||||
|
||||
fun List<BodyRange>?.toBodyRangeList(): BodyRangeList? {
|
||||
if (this == null || !FeatureFlags.textFormatting()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val builder = BodyRangeList.newBuilder()
|
||||
|
||||
for (bodyRange in this) {
|
||||
var style: BodyRangeList.BodyRange.Style? = null
|
||||
when (bodyRange.style) {
|
||||
BodyRange.Style.BOLD -> style = BodyRangeList.BodyRange.Style.BOLD
|
||||
BodyRange.Style.ITALIC -> style = BodyRangeList.BodyRange.Style.ITALIC
|
||||
BodyRange.Style.SPOILER -> Unit
|
||||
BodyRange.Style.STRIKETHROUGH -> style = BodyRangeList.BodyRange.Style.STRIKETHROUGH
|
||||
BodyRange.Style.MONOSPACE -> style = BodyRangeList.BodyRange.Style.MONOSPACE
|
||||
else -> Unit
|
||||
}
|
||||
if (style != null) {
|
||||
builder.addStyle(style, bodyRange.start, bodyRange.length)
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
|
@ -179,15 +179,11 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
|||
return super.getUpdateDisplayBody(context, recipientClickHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable BodyRangeList getMessageRanges() {
|
||||
return messageRanges;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMessageRanges() {
|
||||
return messageRanges != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull BodyRangeList requireMessageRanges() {
|
||||
return Objects.requireNonNull(messageRanges);
|
||||
|
|
|
@ -703,8 +703,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
return isJumboji;
|
||||
}
|
||||
|
||||
public boolean hasMessageRanges() {
|
||||
return false;
|
||||
public @Nullable BodyRangeList getMessageRanges() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public @NonNull BodyRangeList requireMessageRanges() {
|
||||
|
|
|
@ -38,7 +38,7 @@ public class Quote {
|
|||
this.mentions = mentions;
|
||||
this.quoteType = quoteType;
|
||||
|
||||
SpannableString spannable = new SpannableString(Util.emptyIfNull(text));
|
||||
SpannableString spannable = SpannableString.valueOf(Util.emptyIfNull(text));
|
||||
MentionAnnotation.setMentionAnnotations(spannable, mentions);
|
||||
|
||||
this.text = spannable;
|
||||
|
@ -48,7 +48,6 @@ public class Quote {
|
|||
return new Quote(id, author, text, missing, updatedAttachment, mentions, quoteType);
|
||||
}
|
||||
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import androidx.annotation.Nullable;
|
|||
import org.thoughtcrime.securesms.database.MessageTypes;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable.Extra;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.util.Preconditions;
|
||||
|
@ -100,6 +101,14 @@ public final class ThreadRecord {
|
|||
return extra;
|
||||
}
|
||||
|
||||
public @Nullable BodyRangeList getBodyRanges() {
|
||||
if (extra != null && extra.getBodyRanges() != null) {
|
||||
return BodyRangeListSerializer.INSTANCE.deserialize(extra.getBodyRanges());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
|
|
@ -49,7 +49,8 @@ import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredExcepti
|
|||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.BodyRange;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
|
@ -231,6 +232,7 @@ public class IndividualSendJob extends PushSendJob {
|
|||
List<SignalServicePreview> previews = getPreviewsFor(message);
|
||||
SignalServiceDataMessage.GiftBadge giftBadge = getGiftBadgeFor(message);
|
||||
SignalServiceDataMessage.Payment payment = getPayment(message);
|
||||
List<BodyRange> bodyRanges = getBodyRanges(message);
|
||||
SignalServiceDataMessage.Builder mediaMessageBuilder = SignalServiceDataMessage.newBuilder()
|
||||
.withBody(message.getBody())
|
||||
.withAttachments(serviceAttachments)
|
||||
|
@ -244,7 +246,8 @@ public class IndividualSendJob extends PushSendJob {
|
|||
.withGiftBadge(giftBadge)
|
||||
.asExpirationUpdate(message.isExpirationUpdate())
|
||||
.asEndSessionMessage(message.isEndSession())
|
||||
.withPayment(payment);
|
||||
.withPayment(payment)
|
||||
.withBodyRanges(bodyRanges);
|
||||
|
||||
if (message.getParentStoryId() != null) {
|
||||
try {
|
||||
|
@ -316,12 +319,12 @@ public class IndividualSendJob extends PushSendJob {
|
|||
|
||||
return new SignalServiceDataMessage.Payment(new SignalServiceDataMessage.PaymentNotification(payment.getReceipt(), payment.getNote()), null);
|
||||
} else {
|
||||
SignalServiceProtos.DataMessage.Payment.Activation.Type type = null;
|
||||
DataMessage.Payment.Activation.Type type = null;
|
||||
|
||||
if (message.isRequestToActivatePayments()) {
|
||||
type = SignalServiceProtos.DataMessage.Payment.Activation.Type.REQUEST;
|
||||
type = DataMessage.Payment.Activation.Type.REQUEST;
|
||||
} else if (message.isPaymentsActivated()) {
|
||||
type = SignalServiceProtos.DataMessage.Payment.Activation.Type.ACTIVATED;
|
||||
type = DataMessage.Payment.Activation.Type.ACTIVATED;
|
||||
}
|
||||
|
||||
if (type != null) {
|
||||
|
|
|
@ -39,6 +39,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
|||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
@ -196,16 +197,17 @@ public final class PushDistributionListSendJob extends PushSendJob {
|
|||
try {
|
||||
rotateSenderCertificateIfNecessary();
|
||||
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
|
||||
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
|
||||
boolean isRecipientUpdate = Stream.of(SignalDatabase.groupReceipts().getGroupReceiptInfo(messageId))
|
||||
.anyMatch(info -> info.getStatus() > GroupReceiptTable.STATUS_UNDELIVERED);
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
|
||||
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
|
||||
List<SignalServiceProtos.BodyRange> bodyRanges = getBodyRanges(message);
|
||||
boolean isRecipientUpdate = Stream.of(SignalDatabase.groupReceipts().getGroupReceiptInfo(messageId))
|
||||
.anyMatch(info -> info.getStatus() > GroupReceiptTable.STATUS_UNDELIVERED);
|
||||
|
||||
final SignalServiceStoryMessage storyMessage;
|
||||
if (message.getStoryType().isTextStory()) {
|
||||
storyMessage = SignalServiceStoryMessage.forTextAttachment(Recipient.self().getProfileKey(), null, StorySendUtil.deserializeBodyToStoryTextAttachment(message, this::getPreviewsFor), message.getStoryType().isStoryWithReplies());
|
||||
storyMessage = SignalServiceStoryMessage.forTextAttachment(Recipient.self().getProfileKey(), null, StorySendUtil.deserializeBodyToStoryTextAttachment(message, this::getPreviewsFor), message.getStoryType().isStoryWithReplies(), bodyRanges);
|
||||
} else if (!attachmentPointers.isEmpty()) {
|
||||
storyMessage = SignalServiceStoryMessage.forFileAttachment(Recipient.self().getProfileKey(), null, attachmentPointers.get(0), message.getStoryType().isStoryWithReplies());
|
||||
storyMessage = SignalServiceStoryMessage.forFileAttachment(Recipient.self().getProfileKey(), null, attachmentPointers.get(0), message.getStoryType().isStoryWithReplies(), bodyRanges);
|
||||
} else {
|
||||
throw new UndeliverableMessageException("No attachment on non-text story.");
|
||||
}
|
||||
|
|
|
@ -14,9 +14,9 @@ import org.signal.core.util.SetUtil;
|
|||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.database.GroupTable;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptTable;
|
||||
import org.thoughtcrime.securesms.database.GroupReceiptTable.GroupReceiptInfo;
|
||||
import org.thoughtcrime.securesms.database.GroupTable;
|
||||
import org.thoughtcrime.securesms.database.MessageTable;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.RecipientTable;
|
||||
|
@ -58,6 +58,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
|||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.BodyRange;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.GroupContextV2;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -247,10 +248,11 @@ public final class PushGroupSendJob extends PushSendJob {
|
|||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
List<SignalServicePreview> previews = getPreviewsFor(message);
|
||||
List<SignalServiceDataMessage.Mention> mentions = getMentionsFor(message.getMentions());
|
||||
List<BodyRange> bodyRanges = getBodyRanges(message);
|
||||
List<Attachment> attachments = Stream.of(message.getAttachments()).filterNot(Attachment::isSticker).toList();
|
||||
List<SignalServiceAttachment> attachmentPointers = getAttachmentPointersFor(attachments);
|
||||
boolean isRecipientUpdate = Stream.of(SignalDatabase.groupReceipts().getGroupReceiptInfo(messageId))
|
||||
.anyMatch(info -> info.getStatus() > GroupReceiptTable.STATUS_UNDELIVERED);
|
||||
boolean isRecipientUpdate = Stream.of(SignalDatabase.groupReceipts().getGroupReceiptInfo(messageId))
|
||||
.anyMatch(info -> info.getStatus() > GroupReceiptTable.STATUS_UNDELIVERED);
|
||||
|
||||
if (message.getStoryType().isStory()) {
|
||||
Optional<GroupRecord> groupRecord = SignalDatabase.groups().getGroup(groupId);
|
||||
|
@ -267,10 +269,13 @@ public final class PushGroupSendJob extends PushSendJob {
|
|||
|
||||
final SignalServiceStoryMessage storyMessage;
|
||||
if (message.getStoryType().isTextStory()) {
|
||||
storyMessage = SignalServiceStoryMessage.forTextAttachment(Recipient.self().getProfileKey(), groupContext, StorySendUtil.deserializeBodyToStoryTextAttachment(message, this::getPreviewsFor), message.getStoryType()
|
||||
.isStoryWithReplies());
|
||||
storyMessage = SignalServiceStoryMessage.forTextAttachment(Recipient.self().getProfileKey(),
|
||||
groupContext,
|
||||
StorySendUtil.deserializeBodyToStoryTextAttachment(message, this::getPreviewsFor),
|
||||
message.getStoryType().isStoryWithReplies(),
|
||||
bodyRanges);
|
||||
} else if (!attachmentPointers.isEmpty()) {
|
||||
storyMessage = SignalServiceStoryMessage.forFileAttachment(Recipient.self().getProfileKey(), groupContext, attachmentPointers.get(0), message.getStoryType().isStoryWithReplies());
|
||||
storyMessage = SignalServiceStoryMessage.forFileAttachment(Recipient.self().getProfileKey(), groupContext, attachmentPointers.get(0), message.getStoryType().isStoryWithReplies(), bodyRanges);
|
||||
} else {
|
||||
throw new UndeliverableMessageException("No attachment on non-text story.");
|
||||
}
|
||||
|
@ -322,7 +327,8 @@ public final class PushGroupSendJob extends PushSendJob {
|
|||
.withSticker(sticker.orElse(null))
|
||||
.withSharedContacts(sharedContacts)
|
||||
.withPreviews(previews)
|
||||
.withMentions(mentions);
|
||||
.withMentions(mentions)
|
||||
.withBodyRanges(bodyRanges);
|
||||
|
||||
if (message.getParentStoryId() != null) {
|
||||
try {
|
||||
|
|
|
@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase;
|
|||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.ParentStoryId;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
|
@ -65,6 +66,7 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
|||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
|
@ -78,6 +80,7 @@ import java.util.Optional;
|
|||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public abstract class PushSendJob extends SendJob {
|
||||
|
||||
|
@ -296,6 +299,7 @@ public abstract class PushSendJob extends SendJob {
|
|||
String quoteBody = message.getOutgoingQuote().getText();
|
||||
RecipientId quoteAuthor = message.getOutgoingQuote().getAuthor();
|
||||
List<SignalServiceDataMessage.Mention> quoteMentions = getMentionsFor(message.getOutgoingQuote().getMentions());
|
||||
List<SignalServiceProtos.BodyRange> bodyRanges = getBodyRanges(message.getOutgoingQuote().getBodyRanges());
|
||||
QuoteModel.Type quoteType = message.getOutgoingQuote().getType();
|
||||
List<SignalServiceDataMessage.Quote.QuotedAttachment> quoteAttachments = new LinkedList<>();
|
||||
Optional<Attachment> localQuoteAttachment = message.getOutgoingQuote()
|
||||
|
@ -344,9 +348,9 @@ public abstract class PushSendJob extends SendJob {
|
|||
Recipient quoteAuthorRecipient = Recipient.resolved(quoteAuthor);
|
||||
|
||||
if (quoteAuthorRecipient.isMaybeRegistered()) {
|
||||
return Optional.of(new SignalServiceDataMessage.Quote(quoteId, RecipientUtil.getOrFetchServiceId(context, quoteAuthorRecipient), quoteBody, quoteAttachments, quoteMentions, quoteType.getDataMessageType()));
|
||||
return Optional.of(new SignalServiceDataMessage.Quote(quoteId, RecipientUtil.getOrFetchServiceId(context, quoteAuthorRecipient), quoteBody, quoteAttachments, quoteMentions, quoteType.getDataMessageType(), bodyRanges));
|
||||
} else if (quoteAuthorRecipient.hasServiceId()) {
|
||||
return Optional.of(new SignalServiceDataMessage.Quote(quoteId, quoteAuthorRecipient.requireServiceId(), quoteBody, quoteAttachments, quoteMentions, quoteType.getDataMessageType()));
|
||||
return Optional.of(new SignalServiceDataMessage.Quote(quoteId, quoteAuthorRecipient.requireServiceId(), quoteBody, quoteAttachments, quoteMentions, quoteType.getDataMessageType(), bodyRanges));
|
||||
} else {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
@ -433,6 +437,51 @@ public abstract class PushSendJob extends SendJob {
|
|||
}
|
||||
}
|
||||
|
||||
protected @Nullable List<SignalServiceProtos.BodyRange> getBodyRanges(@NonNull OutgoingMessage message) {
|
||||
return getBodyRanges(message.getBodyRanges());
|
||||
}
|
||||
|
||||
protected @Nullable List<SignalServiceProtos.BodyRange> getBodyRanges(@Nullable BodyRangeList bodyRanges) {
|
||||
if (bodyRanges == null || bodyRanges.getRangesCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bodyRanges
|
||||
.getRangesList()
|
||||
.stream()
|
||||
.map(range -> {
|
||||
SignalServiceProtos.BodyRange.Builder builder = SignalServiceProtos.BodyRange.newBuilder()
|
||||
.setStart(range.getStart())
|
||||
.setLength(range.getLength());
|
||||
|
||||
if (range.hasStyle()) {
|
||||
switch (range.getStyle()) {
|
||||
case BOLD:
|
||||
builder.setStyle(SignalServiceProtos.BodyRange.Style.BOLD);
|
||||
break;
|
||||
case ITALIC:
|
||||
builder.setStyle(SignalServiceProtos.BodyRange.Style.ITALIC);
|
||||
break;
|
||||
case SPOILER:
|
||||
// Intentionally left blank
|
||||
break;
|
||||
case STRIKETHROUGH:
|
||||
builder.setStyle(SignalServiceProtos.BodyRange.Style.STRIKETHROUGH);
|
||||
break;
|
||||
case MONOSPACE:
|
||||
builder.setStyle(SignalServiceProtos.BodyRange.Style.MONOSPACE);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unrecognized style");
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Only supports style");
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
protected void rotateSenderCertificateIfNecessary() throws IOException {
|
||||
try {
|
||||
Collection<CertificateType> requiredCertificateTypes = SignalStore.phoneNumberPrivacy()
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
package org.thoughtcrime.securesms.mediasend
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parceler
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.parcelize.TypeParceler
|
||||
import org.thoughtcrime.securesms.conversation.MessageSendType
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.MessageSender.PreUploadResult
|
||||
import org.thoughtcrime.securesms.util.ParcelUtil
|
||||
|
||||
/**
|
||||
* A class that lets us nicely format data that we'll send back to [ConversationActivity].
|
||||
|
@ -21,6 +26,7 @@ class MediaSendActivityResult(
|
|||
val messageSendType: MessageSendType,
|
||||
val isViewOnce: Boolean,
|
||||
val mentions: List<Mention>,
|
||||
@TypeParceler<BodyRangeList?, BodyRangeListParceler>() val bodyRanges: BodyRangeList?,
|
||||
val storyType: StoryType
|
||||
) : Parcelable {
|
||||
|
||||
|
@ -40,3 +46,18 @@ class MediaSendActivityResult(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
object BodyRangeListParceler : Parceler<BodyRangeList?> {
|
||||
override fun create(parcel: Parcel): BodyRangeList? {
|
||||
val data: ByteArray? = ParcelUtil.readByteArray(parcel)
|
||||
return if (data != null) {
|
||||
BodyRangeList.parseFrom(data)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun BodyRangeList?.write(parcel: Parcel, flags: Int) {
|
||||
ParcelUtil.writeByteArray(parcel, this?.toByteArray())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.database.AttachmentTable.TransformProperties
|
|||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.keyvalue.StorySend
|
||||
import org.thoughtcrime.securesms.mediasend.CompositeMediaTransform
|
||||
|
@ -76,6 +77,7 @@ class MediaSelectionRepository(context: Context) {
|
|||
singleContact: ContactSearchKey.RecipientSearchKey?,
|
||||
contacts: List<ContactSearchKey.RecipientSearchKey>,
|
||||
mentions: List<Mention>,
|
||||
bodyRanges: BodyRangeList?,
|
||||
sendType: MessageSendType
|
||||
): Maybe<MediaSendActivityResult> {
|
||||
if (isSms && contacts.isNotEmpty()) {
|
||||
|
@ -89,9 +91,10 @@ class MediaSelectionRepository(context: Context) {
|
|||
val isSendingToStories = singleContact?.isStory == true || contacts.any { it.isStory }
|
||||
val sentMediaQuality = if (isSendingToStories) SentMediaQuality.STANDARD else quality
|
||||
|
||||
return Maybe.create<MediaSendActivityResult> { emitter ->
|
||||
return Maybe.create { emitter ->
|
||||
val trimmedBody: String = if (isViewOnce) "" else getTruncatedBody(message?.toString()?.trim()) ?: ""
|
||||
val trimmedMentions: List<Mention> = if (isViewOnce) emptyList() else mentions
|
||||
val trimmedBodyRanges: BodyRangeList? = if (isViewOnce) null else bodyRanges
|
||||
val modelsToTransform: Map<Media, MediaTransform> = buildModelsToTransform(selectedMedia, stateMap, sentMediaQuality)
|
||||
val oldToNewMediaMap: Map<Media, Media> = MediaRepository.transformMediaSync(context, selectedMedia, modelsToTransform)
|
||||
val updatedMedia = oldToNewMediaMap.values.toList()
|
||||
|
@ -119,6 +122,7 @@ class MediaSelectionRepository(context: Context) {
|
|||
messageSendType = sendType,
|
||||
isViewOnce = isViewOnce,
|
||||
mentions = trimmedMentions,
|
||||
bodyRanges = trimmedBodyRanges,
|
||||
storyType = StoryType.NONE
|
||||
)
|
||||
)
|
||||
|
@ -154,7 +158,7 @@ class MediaSelectionRepository(context: Context) {
|
|||
uploadRepository.updateDisplayOrder(updatedMedia)
|
||||
uploadRepository.getPreUploadResults { uploadResults ->
|
||||
if (contacts.isNotEmpty()) {
|
||||
sendMessages(contacts, splitBody, uploadResults, trimmedMentions, isViewOnce, clippedVideosForStories)
|
||||
sendMessages(contacts, splitBody, uploadResults, trimmedMentions, trimmedBodyRanges, isViewOnce, clippedVideosForStories)
|
||||
uploadRepository.deleteAbandonedAttachments()
|
||||
emitter.onComplete()
|
||||
} else if (uploadResults.isNotEmpty()) {
|
||||
|
@ -166,6 +170,7 @@ class MediaSelectionRepository(context: Context) {
|
|||
messageSendType = sendType,
|
||||
isViewOnce = isViewOnce,
|
||||
mentions = trimmedMentions,
|
||||
bodyRanges = trimmedBodyRanges,
|
||||
storyType = storyType
|
||||
)
|
||||
)
|
||||
|
@ -179,6 +184,7 @@ class MediaSelectionRepository(context: Context) {
|
|||
messageSendType = sendType,
|
||||
isViewOnce = isViewOnce,
|
||||
mentions = trimmedMentions,
|
||||
bodyRanges = trimmedBodyRanges,
|
||||
storyType = storyType
|
||||
)
|
||||
)
|
||||
|
@ -256,6 +262,7 @@ class MediaSelectionRepository(context: Context) {
|
|||
body: String,
|
||||
preUploadResults: Collection<PreUploadResult>,
|
||||
mentions: List<Mention>,
|
||||
bodyRanges: BodyRangeList?,
|
||||
isViewOnce: Boolean,
|
||||
storyClips: List<Media>
|
||||
) {
|
||||
|
@ -287,6 +294,7 @@ class MediaSelectionRepository(context: Context) {
|
|||
isViewOnce = isViewOnce,
|
||||
storyType = storyType,
|
||||
mentions = mentions,
|
||||
bodyRanges = bodyRanges,
|
||||
isSecure = true
|
||||
)
|
||||
|
||||
|
@ -317,6 +325,7 @@ class MediaSelectionRepository(context: Context) {
|
|||
isViewOnce = isViewOnce,
|
||||
storyType = storyType,
|
||||
mentions = mentions,
|
||||
bodyRanges = bodyRanges,
|
||||
isSecure = true
|
||||
)
|
||||
)
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.signal.core.util.BreakIteratorCompat
|
|||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.conversation.MessageSendType
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult
|
||||
import org.thoughtcrime.securesms.mediasend.VideoEditorFragment
|
||||
|
@ -336,16 +337,17 @@ class MediaSelectionViewModel(
|
|||
): Maybe<MediaSendActivityResult> {
|
||||
return UntrustedRecords.checkForBadIdentityRecords(selectedContacts.toSet(), identityChangesSince).andThen(
|
||||
repository.send(
|
||||
store.state.selectedMedia,
|
||||
store.state.editorStateMap,
|
||||
store.state.quality,
|
||||
store.state.message,
|
||||
store.state.sendType.usesSmsTransport,
|
||||
isViewOnceEnabled(),
|
||||
destination.getRecipientSearchKey(),
|
||||
selectedContacts.ifEmpty { destination.getRecipientSearchKeyList() },
|
||||
MentionAnnotation.getMentionsFromAnnotations(store.state.message),
|
||||
store.state.sendType
|
||||
selectedMedia = store.state.selectedMedia,
|
||||
stateMap = store.state.editorStateMap,
|
||||
quality = store.state.quality,
|
||||
message = store.state.message,
|
||||
isSms = store.state.sendType.usesSmsTransport,
|
||||
isViewOnce = isViewOnceEnabled(),
|
||||
singleContact = destination.getRecipientSearchKey(),
|
||||
contacts = selectedContacts.ifEmpty { destination.getRecipientSearchKeyList() },
|
||||
mentions = MentionAnnotation.getMentionsFromAnnotations(store.state.message),
|
||||
bodyRanges = MessageStyler.getStyling(store.state.message),
|
||||
sendType = store.state.sendType
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.database.SentStorySyncManifest;
|
|||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.StickerTable;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable;
|
||||
import org.thoughtcrime.securesms.database.model.DatabaseProtosUtil;
|
||||
import org.thoughtcrime.securesms.database.model.DistributionListId;
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
|
@ -62,6 +63,7 @@ import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
|||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StoryType;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost;
|
||||
|
@ -170,6 +172,7 @@ import org.whispersystems.signalservice.api.payments.Money;
|
|||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.SyncMessage;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -274,7 +277,7 @@ public final class MessageContentProcessor {
|
|||
if (content.getDataMessage().isPresent()) {
|
||||
GroupTable groupDatabase = SignalDatabase.groups();
|
||||
SignalServiceDataMessage message = content.getDataMessage().get();
|
||||
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent() || message.getMentions().isPresent();
|
||||
boolean isMediaMessage = message.getAttachments().isPresent() || message.getQuote().isPresent() || message.getSharedContacts().isPresent() || message.getPreviews().isPresent() || message.getSticker().isPresent() || message.getMentions().isPresent() || message.getBodyRanges().isPresent();
|
||||
Optional<GroupId> groupId = GroupUtil.idFromGroupContext(message.getGroupContext());
|
||||
boolean isGv2Message = groupId.isPresent() && groupId.get().isV2();
|
||||
|
||||
|
@ -1550,7 +1553,8 @@ public final class MessageContentProcessor {
|
|||
content.getServerUuid(),
|
||||
null,
|
||||
false,
|
||||
false);
|
||||
false,
|
||||
getBodyRangeList(message.getBodyRanges()));
|
||||
|
||||
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
||||
|
||||
|
@ -1671,13 +1675,15 @@ public final class MessageContentProcessor {
|
|||
} else if (SignalDatabase.storySends().canReply(senderRecipient.getId(), storyContext.getSentTimestamp())) {
|
||||
MmsMessageRecord story = (MmsMessageRecord) database.getMessageRecord(storyMessageId.getId());
|
||||
|
||||
String displayText = "";
|
||||
String displayText = "";
|
||||
BodyRangeList bodyRanges = null;
|
||||
if (story.getStoryType().isTextStory()) {
|
||||
displayText = story.getBody();
|
||||
bodyRanges = story.getMessageRanges();
|
||||
}
|
||||
|
||||
parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId());
|
||||
quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, displayText, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL);
|
||||
quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, displayText, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL, bodyRanges);
|
||||
expiresInMillis = TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds());
|
||||
} else {
|
||||
warn(content.getTimestamp(), "Story has reactions disabled. Dropping reaction.");
|
||||
|
@ -1780,12 +1786,14 @@ public final class MessageContentProcessor {
|
|||
} else if (groupStory || SignalDatabase.storySends().canReply(senderRecipient.getId(), storyContext.getSentTimestamp())) {
|
||||
parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId());
|
||||
|
||||
String displayText = "";
|
||||
String displayText = "";
|
||||
BodyRangeList bodyRanges = null;
|
||||
if (story.getStoryType().isTextStory()) {
|
||||
displayText = story.getBody();
|
||||
bodyRanges = story.getMessageRanges();
|
||||
}
|
||||
|
||||
quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, displayText, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL);
|
||||
quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, displayText, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL, bodyRanges);
|
||||
expiresInMillis = TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds());
|
||||
} else {
|
||||
warn(content.getTimestamp(), "Story has replies disabled. Dropping reply.");
|
||||
|
@ -1796,6 +1804,8 @@ public final class MessageContentProcessor {
|
|||
return null;
|
||||
}
|
||||
|
||||
BodyRangeList bodyRanges = message.getBodyRanges().map(DatabaseProtosUtil::toBodyRangeList).orElse(null);
|
||||
|
||||
IncomingMediaMessage mediaMessage = new IncomingMediaMessage(senderRecipient.getId(),
|
||||
content.getTimestamp(),
|
||||
content.getServerReceivedTimestamp(),
|
||||
|
@ -1819,7 +1829,8 @@ public final class MessageContentProcessor {
|
|||
content.getServerUuid(),
|
||||
null,
|
||||
false,
|
||||
false);
|
||||
false,
|
||||
bodyRanges);
|
||||
|
||||
Optional<InsertResult> insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
||||
|
||||
|
@ -1934,6 +1945,7 @@ public final class MessageContentProcessor {
|
|||
Optional<List<LinkPreview>> linkPreviews = getLinkPreviews(message.getPreviews(), message.getBody().orElse(""), false);
|
||||
Optional<List<Mention>> mentions = getMentions(message.getMentions());
|
||||
Optional<Attachment> sticker = getStickerAttachment(message.getSticker());
|
||||
BodyRangeList messageRanges = getBodyRangeList(message.getBodyRanges());
|
||||
|
||||
handlePossibleExpirationUpdate(content, message, Optional.empty(), senderRecipient, threadRecipient, receivedTime);
|
||||
|
||||
|
@ -1960,7 +1972,8 @@ public final class MessageContentProcessor {
|
|||
content.getServerUuid(),
|
||||
null,
|
||||
false,
|
||||
false);
|
||||
false,
|
||||
messageRanges);
|
||||
|
||||
insertResult = database.insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
||||
|
||||
|
@ -2064,12 +2077,15 @@ public final class MessageContentProcessor {
|
|||
} else if (groupStory || story.getStoryType().isStoryWithReplies()) {
|
||||
parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId());
|
||||
|
||||
String quoteBody = "";
|
||||
String quoteBody = "";
|
||||
BodyRangeList bodyRanges = null;
|
||||
|
||||
if (story.getStoryType().isTextStory()) {
|
||||
quoteBody = story.getBody();
|
||||
quoteBody = story.getBody();
|
||||
bodyRanges = story.getMessageRanges();
|
||||
}
|
||||
|
||||
quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, quoteBody, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL);
|
||||
quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, quoteBody, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL, bodyRanges);
|
||||
expiresInMillis = TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds());
|
||||
} else {
|
||||
warn(envelopeTimestamp, "Story has replies disabled. Dropping reply.");
|
||||
|
@ -2094,7 +2110,8 @@ public final class MessageContentProcessor {
|
|||
Collections.emptySet(),
|
||||
Collections.emptySet(),
|
||||
null,
|
||||
true);
|
||||
true,
|
||||
null);
|
||||
|
||||
if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
|
||||
handleSynchronizeSentExpirationUpdate(message);
|
||||
|
@ -2213,7 +2230,8 @@ public final class MessageContentProcessor {
|
|||
Collections.emptySet(),
|
||||
Collections.emptySet(),
|
||||
null,
|
||||
true);
|
||||
true,
|
||||
null);
|
||||
|
||||
MessageTable messageTable = SignalDatabase.messages();
|
||||
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
|
||||
|
@ -2310,7 +2328,8 @@ public final class MessageContentProcessor {
|
|||
Collections.emptySet(),
|
||||
Collections.emptySet(),
|
||||
giftBadge.orElse(null),
|
||||
true);
|
||||
true,
|
||||
null);
|
||||
|
||||
if (recipients.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
|
||||
handleSynchronizeSentExpirationUpdate(message);
|
||||
|
@ -2488,9 +2507,10 @@ public final class MessageContentProcessor {
|
|||
{
|
||||
log(envelopeTimestamp, "Synchronize sent text message for " + message.getTimestamp());
|
||||
|
||||
Recipient recipient = getSyncMessageDestination(message);
|
||||
String body = message.getDataMessage().get().getBody().orElse("");
|
||||
long expiresInMillis = TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds());
|
||||
Recipient recipient = getSyncMessageDestination(message);
|
||||
String body = message.getDataMessage().get().getBody().orElse("");
|
||||
long expiresInMillis = TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds());
|
||||
BodyRangeList bodyRanges = message.getDataMessage().get().getBodyRanges().map(DatabaseProtosUtil::toBodyRangeList).orElse(null);
|
||||
|
||||
if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
|
||||
handleSynchronizeSentExpirationUpdate(message);
|
||||
|
@ -2513,14 +2533,15 @@ public final class MessageContentProcessor {
|
|||
StoryType.NONE,
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
true);
|
||||
true,
|
||||
bodyRanges);
|
||||
|
||||
messageId = SignalDatabase.messages().insertMessageOutbox(outgoingMessage, threadId, false, GroupReceiptTable.STATUS_UNKNOWN, null);
|
||||
database = SignalDatabase.messages();
|
||||
|
||||
updateGroupReceiptStatus(message, messageId, recipient.requireGroupId());
|
||||
} else {
|
||||
OutgoingMessage outgoingTextMessage = OutgoingMessage.text(recipient, body, expiresInMillis, message.getTimestamp());
|
||||
OutgoingMessage outgoingTextMessage = OutgoingMessage.text(recipient, body, expiresInMillis, message.getTimestamp(), bodyRanges);
|
||||
|
||||
messageId = SignalDatabase.messages().insertMessageOutbox(outgoingTextMessage, threadId, false, null);
|
||||
database = SignalDatabase.messages();
|
||||
|
@ -3023,7 +3044,7 @@ public final class MessageContentProcessor {
|
|||
|
||||
String body = message.isPaymentNotification() ? message.getDisplayBody(context).toString() : message.getBody();
|
||||
|
||||
return Optional.of(new QuoteModel(quote.get().getId(), author, body, false, attachments, mentions, QuoteModel.Type.fromDataMessageType(quote.get().getType())));
|
||||
return Optional.of(new QuoteModel(quote.get().getId(), author, body, false, attachments, mentions, QuoteModel.Type.fromDataMessageType(quote.get().getType()), message.getMessageRanges()));
|
||||
} else if (message != null) {
|
||||
warn("Found the target for the quote, but it's flagged as remotely deleted.");
|
||||
}
|
||||
|
@ -3036,7 +3057,8 @@ public final class MessageContentProcessor {
|
|||
true,
|
||||
PointerAttachment.forPointers(quote.get().getAttachments()),
|
||||
getMentions(quote.get().getMentions()),
|
||||
QuoteModel.Type.fromDataMessageType(quote.get().getType())));
|
||||
QuoteModel.Type.fromDataMessageType(quote.get().getType()),
|
||||
DatabaseProtosUtil.toBodyRangeList(quote.get().getBodyRanges())));
|
||||
}
|
||||
|
||||
private Optional<Attachment> getStickerAttachment(Optional<SignalServiceDataMessage.Sticker> sticker) {
|
||||
|
@ -3124,6 +3146,14 @@ public final class MessageContentProcessor {
|
|||
return Optional.of(getMentions(signalServiceMentions.get()));
|
||||
}
|
||||
|
||||
private @Nullable BodyRangeList getBodyRangeList(Optional<List<SignalServiceProtos.BodyRange>> bodyRanges) {
|
||||
if (!bodyRanges.isPresent()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return DatabaseProtosUtil.toBodyRangeList(bodyRanges.get());
|
||||
}
|
||||
|
||||
private @NonNull List<Mention> getMentions(@Nullable List<SignalServiceDataMessage.Mention> signalServiceMentions) {
|
||||
if (signalServiceMentions == null || signalServiceMentions.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
|
|
|
@ -90,6 +90,7 @@ class IncomingMediaMessage(
|
|||
isPaymentsActivated = paymentsActivated
|
||||
)
|
||||
|
||||
@JvmOverloads
|
||||
constructor(
|
||||
from: RecipientId?,
|
||||
sentTimeMillis: Long,
|
||||
|
@ -114,7 +115,8 @@ class IncomingMediaMessage(
|
|||
serverGuid: String?,
|
||||
giftBadge: GiftBadge?,
|
||||
activatePaymentsRequest: Boolean,
|
||||
paymentsActivated: Boolean
|
||||
paymentsActivated: Boolean,
|
||||
messageRanges: BodyRangeList? = null
|
||||
) : this(
|
||||
from = from,
|
||||
groupId = if (group.isPresent) GroupId.v2(group.get().masterKey) else null,
|
||||
|
@ -139,7 +141,8 @@ class IncomingMediaMessage(
|
|||
mentions = mentions.orElse(emptyList()),
|
||||
giftBadge = giftBadge,
|
||||
isActivatePaymentsRequest = activatePaymentsRequest,
|
||||
isPaymentsActivated = paymentsActivated
|
||||
isPaymentsActivated = paymentsActivated,
|
||||
messageRanges = messageRanges
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.database.documents.NetworkFailure
|
|||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.ParentStoryId
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
|
@ -34,6 +35,7 @@ data class OutgoingMessage(
|
|||
val attachments: List<Attachment> = emptyList(),
|
||||
val sharedContacts: List<Contact> = emptyList(),
|
||||
val linkPreviews: List<LinkPreview> = emptyList(),
|
||||
val bodyRanges: BodyRangeList? = null,
|
||||
val mentions: List<Mention> = emptyList(),
|
||||
val isGroup: Boolean = false,
|
||||
val isGroupUpdate: Boolean = false,
|
||||
|
@ -75,7 +77,8 @@ data class OutgoingMessage(
|
|||
networkFailures: Set<NetworkFailure> = emptySet(),
|
||||
mismatches: Set<IdentityKeyMismatch> = emptySet(),
|
||||
giftBadge: GiftBadge? = null,
|
||||
isSecure: Boolean = false
|
||||
isSecure: Boolean = false,
|
||||
bodyRanges: BodyRangeList? = null
|
||||
) : this(
|
||||
recipient = recipient,
|
||||
body = body ?: "",
|
||||
|
@ -95,7 +98,8 @@ data class OutgoingMessage(
|
|||
networkFailures = networkFailures,
|
||||
identityKeyMismatches = mismatches,
|
||||
giftBadge = giftBadge,
|
||||
isSecure = isSecure
|
||||
isSecure = isSecure,
|
||||
bodyRanges = bodyRanges
|
||||
)
|
||||
|
||||
/**
|
||||
|
@ -112,7 +116,8 @@ data class OutgoingMessage(
|
|||
storyType: StoryType = StoryType.NONE,
|
||||
linkPreviews: List<LinkPreview> = emptyList(),
|
||||
mentions: List<Mention> = emptyList(),
|
||||
isSecure: Boolean = false
|
||||
isSecure: Boolean = false,
|
||||
bodyRanges: BodyRangeList? = null
|
||||
) : this(
|
||||
recipient = recipient,
|
||||
body = buildMessage(slideDeck, body ?: ""),
|
||||
|
@ -124,7 +129,8 @@ data class OutgoingMessage(
|
|||
storyType = storyType,
|
||||
linkPreviews = linkPreviews,
|
||||
mentions = mentions,
|
||||
isSecure = isSecure
|
||||
isSecure = isSecure,
|
||||
bodyRanges = bodyRanges
|
||||
)
|
||||
|
||||
fun withExpiry(expiresIn: Long): OutgoingMessage {
|
||||
|
@ -167,14 +173,21 @@ data class OutgoingMessage(
|
|||
* A secure message that only contains text.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun text(recipient: Recipient, body: String, expiresIn: Long, sentTimeMillis: Long = System.currentTimeMillis()): OutgoingMessage {
|
||||
fun text(
|
||||
recipient: Recipient,
|
||||
body: String,
|
||||
expiresIn: Long,
|
||||
sentTimeMillis: Long = System.currentTimeMillis(),
|
||||
bodyRanges: BodyRangeList? = null
|
||||
): OutgoingMessage {
|
||||
return OutgoingMessage(
|
||||
recipient = recipient,
|
||||
sentTimeMillis = sentTimeMillis,
|
||||
body = body,
|
||||
expiresIn = expiresIn,
|
||||
isUrgent = true,
|
||||
isSecure = true
|
||||
isSecure = true,
|
||||
bodyRanges = bodyRanges
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -239,7 +252,8 @@ data class OutgoingMessage(
|
|||
body: String,
|
||||
sentTimeMillis: Long,
|
||||
storyType: StoryType,
|
||||
linkPreviews: List<LinkPreview>
|
||||
linkPreviews: List<LinkPreview>,
|
||||
bodyRanges: BodyRangeList?
|
||||
): OutgoingMessage {
|
||||
return OutgoingMessage(
|
||||
recipient = recipient,
|
||||
|
@ -247,6 +261,7 @@ data class OutgoingMessage(
|
|||
sentTimeMillis = sentTimeMillis,
|
||||
storyType = storyType,
|
||||
linkPreviews = linkPreviews,
|
||||
bodyRanges = bodyRanges,
|
||||
isSecure = true
|
||||
)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
|
||||
|
@ -21,6 +22,7 @@ public class QuoteModel {
|
|||
private final List<Attachment> attachments;
|
||||
private final List<Mention> mentions;
|
||||
private final Type type;
|
||||
private final BodyRangeList bodyRanges;
|
||||
|
||||
public QuoteModel(long id,
|
||||
@NonNull RecipientId author,
|
||||
|
@ -28,7 +30,8 @@ public class QuoteModel {
|
|||
boolean missing,
|
||||
@Nullable List<Attachment> attachments,
|
||||
@Nullable List<Mention> mentions,
|
||||
@NonNull Type type)
|
||||
@NonNull Type type,
|
||||
@Nullable BodyRangeList bodyRanges)
|
||||
{
|
||||
this.id = id;
|
||||
this.author = author;
|
||||
|
@ -37,6 +40,7 @@ public class QuoteModel {
|
|||
this.attachments = attachments;
|
||||
this.mentions = mentions != null ? mentions : Collections.emptyList();
|
||||
this.type = type;
|
||||
this.bodyRanges = bodyRanges;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
|
@ -67,6 +71,10 @@ public class QuoteModel {
|
|||
return type;
|
||||
}
|
||||
|
||||
public @Nullable BodyRangeList getBodyRanges() {
|
||||
return bodyRanges;
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
NORMAL(0, SignalServiceDataMessage.Quote.Type.NORMAL),
|
||||
GIFT_BADGE(1, SignalServiceDataMessage.Quote.Type.GIFT_BADGE);
|
||||
|
|
|
@ -100,12 +100,13 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
|||
Collections.emptySet(),
|
||||
Collections.emptySet(),
|
||||
null,
|
||||
recipient.isPushGroup());
|
||||
recipient.isPushGroup(),
|
||||
null);
|
||||
threadId = MessageSender.send(context, reply, -1, MessageSender.SendType.SIGNAL, null, null);
|
||||
break;
|
||||
}
|
||||
case SecureMessage: {
|
||||
OutgoingMessage reply = OutgoingMessage.text(recipient, responseText.toString(), expiresIn, System.currentTimeMillis());
|
||||
OutgoingMessage reply = OutgoingMessage.text(recipient, responseText.toString(), expiresIn, System.currentTimeMillis(), null);
|
||||
threadId = MessageSender.send(context, reply, -1, MessageSender.SendType.SIGNAL, null, null);
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -193,17 +193,17 @@ class MessageNotification(threadRecipient: Recipient, record: MessageRecord) : N
|
|||
} else if (record.isRemoteDelete) {
|
||||
SpanUtil.italic(context.getString(R.string.MessageNotifier_this_message_was_deleted))
|
||||
} else if (record.isMms && !record.isMmsNotification && (record as MmsMessageRecord).slideDeck.slides.isNotEmpty()) {
|
||||
ThreadBodyUtil.getFormattedBodyFor(context, record)
|
||||
ThreadBodyUtil.getFormattedBodyFor(context, record).body
|
||||
} else if (record.isGroupCall) {
|
||||
MessageRecord.getGroupCallUpdateDescription(context, record.body, false).spannable
|
||||
} else if (record.hasGiftBadge()) {
|
||||
ThreadBodyUtil.getFormattedBodyFor(context, record)
|
||||
ThreadBodyUtil.getFormattedBodyFor(context, record).body
|
||||
} else if (record.isStoryReaction()) {
|
||||
ThreadBodyUtil.getFormattedBodyFor(context, record)
|
||||
ThreadBodyUtil.getFormattedBodyFor(context, record).body
|
||||
} else if (record.isPaymentNotification()) {
|
||||
ThreadBodyUtil.getFormattedBodyFor(context, record)
|
||||
ThreadBodyUtil.getFormattedBodyFor(context, record).body
|
||||
} else {
|
||||
MentionUtil.updateBodyWithDisplayNames(context, record)
|
||||
MentionUtil.updateBodyWithDisplayNames(context, record) ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -299,7 +299,7 @@ class ReactionNotification(threadRecipient: Recipient, record: MessageRecord, va
|
|||
}
|
||||
|
||||
private fun getReactionMessageBody(context: Context): CharSequence {
|
||||
val body: CharSequence = MentionUtil.updateBodyWithDisplayNames(context, record)
|
||||
val body: CharSequence = MentionUtil.updateBodyWithDisplayNames(context, record) ?: ""
|
||||
val bodyIsEmpty: Boolean = TextUtils.isEmpty(body)
|
||||
|
||||
return if (record.hasSharedContact()) {
|
||||
|
|
|
@ -8,8 +8,8 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
|||
data class MessageResult(
|
||||
val conversationRecipient: Recipient,
|
||||
val messageRecipient: Recipient,
|
||||
val body: String,
|
||||
val bodySnippet: String,
|
||||
val body: CharSequence,
|
||||
val bodySnippet: CharSequence,
|
||||
val threadId: Long,
|
||||
val messageId: Long,
|
||||
val receivedTimestampMs: Long,
|
||||
|
|
|
@ -3,6 +3,9 @@ package org.thoughtcrime.securesms.search;
|
|||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.MergeCursor;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -12,10 +15,14 @@ import androidx.annotation.WorkerThread;
|
|||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.CursorUtil;
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.core.util.concurrent.LatestPrioritizedSerialExecutor;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.contacts.ContactRepository;
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||
import org.thoughtcrime.securesms.database.BodyAdjustment;
|
||||
import org.thoughtcrime.securesms.database.BodyRangeUtil;
|
||||
import org.thoughtcrime.securesms.database.GroupTable;
|
||||
import org.thoughtcrime.securesms.database.MentionTable;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil;
|
||||
|
@ -28,6 +35,7 @@ import org.thoughtcrime.securesms.database.model.GroupRecord;
|
|||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
@ -43,6 +51,7 @@ import java.util.LinkedHashSet;
|
|||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.function.Consumer;
|
||||
|
@ -62,8 +71,8 @@ public class SearchRepository {
|
|||
private final ContactRepository contactRepository;
|
||||
private final ThreadTable threadTable;
|
||||
private final RecipientTable recipientTable;
|
||||
private final MentionTable mentionDatabase;
|
||||
private final MessageTable mmsDatabase;
|
||||
private final MentionTable mentionTable;
|
||||
private final MessageTable messageTable;
|
||||
|
||||
private final LatestPrioritizedSerialExecutor searchExecutor;
|
||||
private final Executor serialExecutor;
|
||||
|
@ -71,11 +80,11 @@ public class SearchRepository {
|
|||
public SearchRepository(@NonNull String noteToSelfTitle) {
|
||||
this.context = ApplicationDependencies.getApplication().getApplicationContext();
|
||||
this.noteToSelfTitle = noteToSelfTitle;
|
||||
this.searchDatabase = SignalDatabase.messageSearch();
|
||||
this.threadTable = SignalDatabase.threads();
|
||||
this.recipientTable = SignalDatabase.recipients();
|
||||
this.mentionDatabase = SignalDatabase.mentions();
|
||||
this.mmsDatabase = SignalDatabase.messages();
|
||||
this.searchDatabase = SignalDatabase.messageSearch();
|
||||
this.threadTable = SignalDatabase.threads();
|
||||
this.recipientTable = SignalDatabase.recipients();
|
||||
this.mentionTable = SignalDatabase.mentions();
|
||||
this.messageTable = SignalDatabase.messages();
|
||||
this.contactRepository = new ContactRepository(context, noteToSelfTitle);
|
||||
this.searchExecutor = new LatestPrioritizedSerialExecutor(SignalExecutors.BOUNDED);
|
||||
this.serialExecutor = new SerialExecutor(SignalExecutors.BOUNDED);
|
||||
|
@ -144,7 +153,7 @@ public class SearchRepository {
|
|||
Cursor textSecureContacts = contactRepository.querySignalContacts(query);
|
||||
Cursor systemContacts = contactRepository.queryNonSignalContacts(query);
|
||||
|
||||
contacts = new MergeCursor(new Cursor[]{ textSecureContacts, systemContacts });
|
||||
contacts = new MergeCursor(new Cursor[] { textSecureContacts, systemContacts });
|
||||
|
||||
return readToList(contacts, new RecipientModelBuilder(), 250);
|
||||
} finally {
|
||||
|
@ -225,21 +234,44 @@ public class SearchRepository {
|
|||
return results;
|
||||
}
|
||||
|
||||
Map<Long, List<Mention>> mentions = SignalDatabase.mentions().getMentionsForMessages(messageIds);
|
||||
if (mentions.isEmpty()) {
|
||||
Map<Long, BodyRangeList> bodyRanges = SignalDatabase.messages().getBodyRangesForMessages(messageIds);
|
||||
Map<Long, List<Mention>> mentions = SignalDatabase.mentions().getMentionsForMessages(messageIds);
|
||||
|
||||
if (bodyRanges.isEmpty() && mentions.isEmpty()) {
|
||||
return results;
|
||||
}
|
||||
|
||||
List<MessageResult> updatedResults = new ArrayList<>(results.size());
|
||||
for (MessageResult result : results) {
|
||||
if (result.isMms() && mentions.containsKey(result.getMessageId())) {
|
||||
List<Mention> messageMentions = mentions.get(result.getMessageId());
|
||||
if (bodyRanges.containsKey(result.getMessageId()) || mentions.containsKey(result.getMessageId())) {
|
||||
CharSequence body = result.getBody();
|
||||
CharSequence bodySnippet = result.getBodySnippet();
|
||||
CharSequence updatedBody = body;
|
||||
List<BodyAdjustment> bodyAdjustments = Collections.emptyList();
|
||||
CharSequence updatedSnippet = bodySnippet;
|
||||
List<BodyAdjustment> snippetAdjustments = Collections.emptyList();
|
||||
List<Mention> messageMentions = mentions.get(result.getMessageId());
|
||||
BodyRangeList ranges = bodyRanges.get(result.getMessageId());
|
||||
|
||||
//noinspection ConstantConditions
|
||||
String updatedBody = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, result.getBody(), messageMentions).getBody().toString();
|
||||
String updatedSnippet = updateSnippetWithDisplayNames(result.getBody(), result.getBodySnippet(), messageMentions);
|
||||
if (messageMentions != null) {
|
||||
MentionUtil.UpdatedBodyAndMentions bodyMentionUpdate = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, body, messageMentions);
|
||||
updatedBody = Objects.requireNonNull(bodyMentionUpdate.getBody());
|
||||
bodyAdjustments = bodyMentionUpdate.getBodyAdjustments();
|
||||
|
||||
MentionUtil.UpdatedBodyAndMentions snippetMentionUpdate = updateSnippetWithDisplayNames(body, bodySnippet, messageMentions);
|
||||
updatedSnippet = Objects.requireNonNull(snippetMentionUpdate.getBody());
|
||||
snippetAdjustments = snippetMentionUpdate.getBodyAdjustments();
|
||||
}
|
||||
|
||||
if (ranges != null) {
|
||||
updatedBody = SpannableString.valueOf(updatedBody);
|
||||
MessageStyler.style(BodyRangeUtil.adjustBodyRanges(ranges, bodyAdjustments), (Spannable) updatedBody);
|
||||
|
||||
updatedSnippet = SpannableString.valueOf(updatedSnippet);
|
||||
//noinspection ConstantConditions
|
||||
updateSnippetWithStyles(updatedBody, (SpannableString) updatedSnippet, BodyRangeUtil.adjustBodyRanges(ranges, snippetAdjustments));
|
||||
}
|
||||
|
||||
//noinspection ConstantConditions
|
||||
updatedResults.add(new MessageResult(result.getConversationRecipient(), result.getMessageRecipient(), updatedBody, updatedSnippet, result.getThreadId(), result.getMessageId(), result.getReceivedTimestampMs(), result.isMms()));
|
||||
} else {
|
||||
updatedResults.add(result);
|
||||
|
@ -249,20 +281,20 @@ public class SearchRepository {
|
|||
return updatedResults;
|
||||
}
|
||||
|
||||
private @NonNull String updateSnippetWithDisplayNames(@NonNull String body, @NonNull String bodySnippet, @NonNull List<Mention> mentions) {
|
||||
String cleanSnippet = bodySnippet;
|
||||
int startOffset = 0;
|
||||
private @NonNull MentionUtil.UpdatedBodyAndMentions updateSnippetWithDisplayNames(@NonNull CharSequence body, @NonNull CharSequence bodySnippet, @NonNull List<Mention> mentions) {
|
||||
CharSequence cleanSnippet = bodySnippet;
|
||||
int startOffset = 0;
|
||||
|
||||
if (cleanSnippet.startsWith(SNIPPET_WRAP)) {
|
||||
cleanSnippet = cleanSnippet.substring(SNIPPET_WRAP.length());
|
||||
if (StringUtil.startsWith(cleanSnippet, SNIPPET_WRAP)) {
|
||||
cleanSnippet = cleanSnippet.subSequence(SNIPPET_WRAP.length(), cleanSnippet.length());
|
||||
startOffset = SNIPPET_WRAP.length();
|
||||
}
|
||||
|
||||
if (cleanSnippet.endsWith(SNIPPET_WRAP)) {
|
||||
cleanSnippet = cleanSnippet.substring(0, cleanSnippet.length() - SNIPPET_WRAP.length());
|
||||
if (StringUtil.endsWith(cleanSnippet, SNIPPET_WRAP)) {
|
||||
cleanSnippet = cleanSnippet.subSequence(0, cleanSnippet.length() - SNIPPET_WRAP.length());
|
||||
}
|
||||
|
||||
int startIndex = body.indexOf(cleanSnippet);
|
||||
int startIndex = TextUtils.indexOf(body, cleanSnippet);
|
||||
|
||||
if (startIndex != -1) {
|
||||
List<Mention> adjustMentions = new ArrayList<>(mentions.size());
|
||||
|
@ -273,11 +305,38 @@ public class SearchRepository {
|
|||
}
|
||||
}
|
||||
|
||||
//noinspection ConstantConditions
|
||||
return MentionUtil.updateBodyAndMentionsWithDisplayNames(context, bodySnippet, adjustMentions).getBody().toString();
|
||||
return MentionUtil.updateBodyAndMentionsWithDisplayNames(context, bodySnippet, adjustMentions);
|
||||
} else {
|
||||
return MentionUtil.updateBodyAndMentionsWithDisplayNames(context, bodySnippet, Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateSnippetWithStyles(@NonNull CharSequence body, @NonNull SpannableString bodySnippet, @NonNull BodyRangeList bodyRanges) {
|
||||
CharSequence cleanSnippet = bodySnippet;
|
||||
int startOffset = 0;
|
||||
|
||||
if (StringUtil.startsWith(cleanSnippet, SNIPPET_WRAP)) {
|
||||
cleanSnippet = cleanSnippet.subSequence(SNIPPET_WRAP.length(), cleanSnippet.length());
|
||||
startOffset = SNIPPET_WRAP.length();
|
||||
}
|
||||
|
||||
return bodySnippet;
|
||||
if (StringUtil.endsWith(cleanSnippet, SNIPPET_WRAP)) {
|
||||
cleanSnippet = cleanSnippet.subSequence(0, cleanSnippet.length() - SNIPPET_WRAP.length());
|
||||
}
|
||||
|
||||
int startIndex = TextUtils.indexOf(body, cleanSnippet);
|
||||
|
||||
if (startIndex != -1) {
|
||||
BodyRangeList.Builder builder = BodyRangeList.newBuilder();
|
||||
for (BodyRangeList.BodyRange range : bodyRanges.getRangesList()) {
|
||||
int adjustedStart = range.getStart() - startIndex + startOffset;
|
||||
if (adjustedStart >= 0 && adjustedStart + range.getLength() <= cleanSnippet.length()) {
|
||||
builder.addRanges(range.toBuilder().setStart(adjustedStart).build());
|
||||
}
|
||||
}
|
||||
|
||||
MessageStyler.style(builder.build(), bodySnippet);
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull List<MessageResult> queryMessages(@NonNull String query, long threadId) {
|
||||
|
@ -294,7 +353,7 @@ public class SearchRepository {
|
|||
}
|
||||
}
|
||||
|
||||
Map<Long, List<Mention>> mentionQueryResults = mentionDatabase.getMentionsContainingRecipients(recipientIds, 500);
|
||||
Map<Long, List<Mention>> mentionQueryResults = mentionTable.getMentionsContainingRecipients(recipientIds, 500);
|
||||
|
||||
if (mentionQueryResults.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
|
@ -302,14 +361,20 @@ public class SearchRepository {
|
|||
|
||||
List<MessageResult> results = new ArrayList<>();
|
||||
|
||||
try (MessageTable.Reader reader = mmsDatabase.getMessages(mentionQueryResults.keySet())) {
|
||||
MessageRecord record;
|
||||
while ((record = reader.getNext()) != null) {
|
||||
List<Mention> mentions = mentionQueryResults.get(record.getId());
|
||||
try (MessageTable.Reader reader = messageTable.getMessages(mentionQueryResults.keySet())) {
|
||||
for (MessageRecord record : reader) {
|
||||
BodyRangeList bodyRanges = record.getMessageRanges();
|
||||
List<Mention> mentions = mentionQueryResults.get(record.getId());
|
||||
|
||||
if (Util.hasItems(mentions)) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, record.getBody(), mentions);
|
||||
String updatedBody = updated.getBody() != null ? updated.getBody().toString() : record.getBody();
|
||||
String updatedSnippet = makeSnippet(cleanQueries, updatedBody);
|
||||
SpannableString body = new SpannableString(record.getBody());
|
||||
|
||||
if (bodyRanges != null) {
|
||||
MessageStyler.style(bodyRanges, body);
|
||||
}
|
||||
|
||||
CharSequence updatedBody = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, body, mentions).getBody();
|
||||
CharSequence updatedSnippet = makeSnippet(cleanQueries, Objects.requireNonNull(updatedBody));
|
||||
|
||||
//noinspection ConstantConditions
|
||||
results.add(new MessageResult(threadTable.getRecipientForThreadId(record.getThreadId()), record.getRecipient(), updatedBody, updatedSnippet, record.getThreadId(), record.getId(), record.getDateReceived(), true));
|
||||
|
@ -328,7 +393,7 @@ public class SearchRepository {
|
|||
}
|
||||
}
|
||||
|
||||
Map<Long, List<Mention>> mentionQueryResults = mentionDatabase.getMentionsContainingRecipients(recipientIds, threadId, 500);
|
||||
Map<Long, List<Mention>> mentionQueryResults = mentionTable.getMentionsContainingRecipients(recipientIds, threadId, 500);
|
||||
|
||||
if (mentionQueryResults.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
|
@ -336,9 +401,8 @@ public class SearchRepository {
|
|||
|
||||
List<MessageResult> results = new ArrayList<>();
|
||||
|
||||
try (MessageTable.Reader reader = mmsDatabase.getMessages(mentionQueryResults.keySet())) {
|
||||
MessageRecord record;
|
||||
while ((record = reader.getNext()) != null) {
|
||||
try (MessageTable.Reader reader = messageTable.getMessages(mentionQueryResults.keySet())) {
|
||||
for (MessageRecord record : reader) {
|
||||
//noinspection ConstantConditions
|
||||
results.add(new MessageResult(threadTable.getRecipientForThreadId(record.getThreadId()), record.getRecipient(), record.getBody(), record.getBody(), record.getThreadId(), record.getId(), record.getDateReceived(), true));
|
||||
}
|
||||
|
@ -347,23 +411,26 @@ public class SearchRepository {
|
|||
return results;
|
||||
}
|
||||
|
||||
private @NonNull String makeSnippet(@NonNull List<String> queries, @NonNull String body) {
|
||||
if (body.length() < 50) {
|
||||
return body;
|
||||
private @NonNull CharSequence makeSnippet(@NonNull List<String> queries, @NonNull CharSequence styledBody) {
|
||||
if (styledBody.length() < 50) {
|
||||
return styledBody;
|
||||
}
|
||||
|
||||
String lowerBody = body.toLowerCase();
|
||||
String lowerBody = styledBody.toString().toLowerCase();
|
||||
for (String query : queries) {
|
||||
int foundIndex = lowerBody.indexOf(query.toLowerCase());
|
||||
if (foundIndex != -1) {
|
||||
int snippetStart = Math.max(0, Math.max(body.lastIndexOf(' ', foundIndex - 5) + 1, foundIndex - 15));
|
||||
int lastSpace = body.indexOf(' ', foundIndex + 30);
|
||||
int snippetEnd = Math.min(body.length(), lastSpace > 0 ? Math.min(lastSpace, foundIndex + 40) : foundIndex + 40);
|
||||
int snippetStart = Math.max(0, Math.max(lowerBody.lastIndexOf(' ', foundIndex - 5) + 1, foundIndex - 15));
|
||||
int lastSpace = lowerBody.indexOf(' ', foundIndex + 30);
|
||||
int snippetEnd = Math.min(lowerBody.length(), lastSpace > 0 ? Math.min(lastSpace, foundIndex + 40) : foundIndex + 40);
|
||||
|
||||
return (snippetStart > 0 ? SNIPPET_WRAP : "") + body.substring(snippetStart, snippetEnd) + (snippetEnd < body.length() ? SNIPPET_WRAP : "");
|
||||
return new SpannableStringBuilder().append(snippetStart > 0 ? SNIPPET_WRAP : "")
|
||||
.append(styledBody.subSequence(snippetStart, snippetEnd))
|
||||
.append(snippetEnd < styledBody.length() ? SNIPPET_WRAP : "");
|
||||
}
|
||||
}
|
||||
return body;
|
||||
|
||||
return styledBody;
|
||||
}
|
||||
|
||||
private @NonNull <T> List<T> readToList(@Nullable Cursor cursor, @NonNull ModelBuilder<T> builder) {
|
||||
|
|
|
@ -9,10 +9,13 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.signal.core.util.BreakIteratorCompat;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator;
|
||||
|
@ -31,6 +34,8 @@ import java.util.stream.Collectors;
|
|||
|
||||
public final class MultiShareArgs implements Parcelable {
|
||||
|
||||
private static final String TAG = Log.tag(MultiShareArgs.class);
|
||||
|
||||
private final Set<ContactSearchKey> contactSearchKeys;
|
||||
private final List<Media> media;
|
||||
private final String draftText;
|
||||
|
@ -44,6 +49,7 @@ public final class MultiShareArgs implements Parcelable {
|
|||
private final long timestamp;
|
||||
private final long expiresAt;
|
||||
private final boolean isTextStory;
|
||||
private final BodyRangeList bodyRanges;
|
||||
|
||||
private MultiShareArgs(@NonNull Builder builder) {
|
||||
contactSearchKeys = builder.contactSearchKeys;
|
||||
|
@ -59,6 +65,7 @@ public final class MultiShareArgs implements Parcelable {
|
|||
timestamp = builder.timestamp;
|
||||
expiresAt = builder.expiresAt;
|
||||
isTextStory = builder.isTextStory;
|
||||
bodyRanges = builder.bodyRanges;
|
||||
}
|
||||
|
||||
protected MultiShareArgs(Parcel in) {
|
||||
|
@ -86,6 +93,17 @@ public final class MultiShareArgs implements Parcelable {
|
|||
}
|
||||
|
||||
linkPreview = preview;
|
||||
|
||||
BodyRangeList bodyRanges = null;
|
||||
try {
|
||||
byte[] data = ParcelUtil.readByteArray(in);
|
||||
if (data != null) {
|
||||
bodyRanges = BodyRangeList.parseFrom(data);
|
||||
}
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "Invalid body range", e);
|
||||
}
|
||||
this.bodyRanges = bodyRanges;
|
||||
}
|
||||
|
||||
public Set<ContactSearchKey> getContactSearchKeys() {
|
||||
|
@ -147,6 +165,10 @@ public final class MultiShareArgs implements Parcelable {
|
|||
return expiresAt;
|
||||
}
|
||||
|
||||
public @Nullable BodyRangeList getBodyRanges() {
|
||||
return bodyRanges;
|
||||
}
|
||||
|
||||
public boolean isValidForStories() {
|
||||
if (isViewOnce()) {
|
||||
return false;
|
||||
|
@ -241,6 +263,12 @@ public final class MultiShareArgs implements Parcelable {
|
|||
} else {
|
||||
dest.writeString("");
|
||||
}
|
||||
|
||||
if (bodyRanges != null) {
|
||||
ParcelUtil.writeByteArray(dest, bodyRanges.toByteArray());
|
||||
} else {
|
||||
ParcelUtil.writeByteArray(dest, null);
|
||||
}
|
||||
}
|
||||
|
||||
public Builder buildUpon() {
|
||||
|
@ -259,7 +287,8 @@ public final class MultiShareArgs implements Parcelable {
|
|||
.withMentions(mentions)
|
||||
.withTimestamp(timestamp)
|
||||
.withExpiration(expiresAt)
|
||||
.asTextStory(isTextStory);
|
||||
.asTextStory(isTextStory)
|
||||
.withBodyRanges(bodyRanges);
|
||||
}
|
||||
|
||||
private boolean requiresInterstitial() {
|
||||
|
@ -286,6 +315,7 @@ public final class MultiShareArgs implements Parcelable {
|
|||
private long timestamp;
|
||||
private long expiresAt;
|
||||
private boolean isTextStory;
|
||||
private BodyRangeList bodyRanges;
|
||||
|
||||
public Builder() {
|
||||
this(Collections.emptySet());
|
||||
|
@ -355,6 +385,11 @@ public final class MultiShareArgs implements Parcelable {
|
|||
return this;
|
||||
}
|
||||
|
||||
public @NonNull Builder withBodyRanges(@Nullable BodyRangeList bodyRanges) {
|
||||
this.bodyRanges = bodyRanges;
|
||||
return this;
|
||||
}
|
||||
|
||||
public @NonNull MultiShareArgs build() {
|
||||
return new MultiShareArgs(this);
|
||||
}
|
||||
|
|
|
@ -250,7 +250,8 @@ public final class MultiShareSender {
|
|||
storyType.toTextStoryType(),
|
||||
buildLinkPreviews(context, multiShareArgs.getLinkPreview()),
|
||||
Collections.emptyList(),
|
||||
false);
|
||||
false,
|
||||
multiShareArgs.getBodyRanges());
|
||||
|
||||
outgoingMessages.add(outgoingMessage);
|
||||
} else if (canSendAsTextStory) {
|
||||
|
@ -286,7 +287,8 @@ public final class MultiShareSender {
|
|||
storyType,
|
||||
Collections.emptyList(),
|
||||
validatedMentions,
|
||||
false);
|
||||
false,
|
||||
multiShareArgs.getBodyRanges());
|
||||
|
||||
outgoingMessages.add(outgoingMessage);
|
||||
}
|
||||
|
@ -302,7 +304,8 @@ public final class MultiShareSender {
|
|||
StoryType.NONE,
|
||||
buildLinkPreviews(context, multiShareArgs.getLinkPreview()),
|
||||
validatedMentions,
|
||||
false);
|
||||
false,
|
||||
multiShareArgs.getBodyRanges());
|
||||
|
||||
outgoingMessages.add(outgoingMessage);
|
||||
}
|
||||
|
@ -392,7 +395,7 @@ public final class MultiShareSender {
|
|||
|
||||
OutgoingMessage outgoingMessage;
|
||||
if (shouldSendAsPush(recipient, forceSms)) {
|
||||
outgoingMessage = OutgoingMessage.text(recipient, body, expiresIn, System.currentTimeMillis());
|
||||
outgoingMessage = OutgoingMessage.text(recipient, body, expiresIn, System.currentTimeMillis(), multiShareArgs.getBodyRanges());
|
||||
} else {
|
||||
outgoingMessage = OutgoingMessage.sms(recipient, body, subscriptionId);
|
||||
}
|
||||
|
@ -419,7 +422,8 @@ public final class MultiShareSender {
|
|||
.toByteArray()),
|
||||
sentTimestamp,
|
||||
storyType.toTextStoryType(),
|
||||
buildLinkPreviews(context, multiShareArgs.getLinkPreview()));
|
||||
buildLinkPreviews(context, multiShareArgs.getLinkPreview()),
|
||||
multiShareArgs.getBodyRanges());
|
||||
}
|
||||
|
||||
private static @NonNull String getBodyForTextStory(@Nullable String draftText, @Nullable LinkPreview linkPreview) {
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
|||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.fonts.TextFont
|
||||
|
@ -42,13 +43,15 @@ import java.security.MessageDigest
|
|||
data class StoryTextPostModel(
|
||||
private val storyTextPost: StoryTextPost,
|
||||
private val storySentAtMillis: Long,
|
||||
private val storyAuthor: RecipientId
|
||||
private val storyAuthor: RecipientId,
|
||||
private val bodyRanges: BodyRangeList?
|
||||
) : Key, Parcelable {
|
||||
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update(storyTextPost.toByteArray())
|
||||
messageDigest.update(storySentAtMillis.toString().toByteArray())
|
||||
messageDigest.update(storyAuthor.serialize().toByteArray())
|
||||
messageDigest.update(bodyRanges?.toByteArray() ?: ByteArray(0))
|
||||
}
|
||||
|
||||
val text: String = storyTextPost.body
|
||||
|
@ -65,6 +68,7 @@ data class StoryTextPostModel(
|
|||
ParcelUtil.writeByteArray(parcel, storyTextPost.toByteArray())
|
||||
parcel.writeLong(storySentAtMillis)
|
||||
parcel.writeParcelable(storyAuthor, flags)
|
||||
ParcelUtil.writeByteArray(parcel, bodyRanges?.toByteArray())
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
|
@ -73,12 +77,11 @@ data class StoryTextPostModel(
|
|||
|
||||
companion object CREATOR : Parcelable.Creator<StoryTextPostModel> {
|
||||
override fun createFromParcel(parcel: Parcel): StoryTextPostModel {
|
||||
val storyTextPostArray = ParcelUtil.readByteArray(parcel)
|
||||
|
||||
return StoryTextPostModel(
|
||||
StoryTextPost.parseFrom(storyTextPostArray),
|
||||
parcel.readLong(),
|
||||
parcel.readParcelable(RecipientId::class.java.classLoader)!!
|
||||
storyTextPost = StoryTextPost.parseFrom(ParcelUtil.readByteArray(parcel)),
|
||||
storySentAtMillis = parcel.readLong(),
|
||||
storyAuthor = parcel.readParcelable(RecipientId::class.java.classLoader)!!,
|
||||
bodyRanges = ParcelUtil.readByteArray(parcel)?.let { BodyRangeList.parseFrom(it) }
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -88,19 +91,21 @@ data class StoryTextPostModel(
|
|||
|
||||
fun parseFrom(messageRecord: MessageRecord): StoryTextPostModel {
|
||||
return parseFrom(
|
||||
messageRecord.body,
|
||||
messageRecord.timestamp,
|
||||
if (messageRecord.isOutgoing) Recipient.self().id else messageRecord.individualRecipient.id
|
||||
body = messageRecord.body,
|
||||
storySentAtMillis = messageRecord.timestamp,
|
||||
storyAuthor = if (messageRecord.isOutgoing) Recipient.self().id else messageRecord.individualRecipient.id,
|
||||
bodyRanges = messageRecord.messageRanges
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Throws(IOException::class)
|
||||
fun parseFrom(body: String, storySentAtMillis: Long, storyAuthor: RecipientId): StoryTextPostModel {
|
||||
fun parseFrom(body: String, storySentAtMillis: Long, storyAuthor: RecipientId, bodyRanges: BodyRangeList?): StoryTextPostModel {
|
||||
return StoryTextPostModel(
|
||||
storyTextPost = StoryTextPost.parseFrom(Base64.decode(body)),
|
||||
storySentAtMillis = storySentAtMillis,
|
||||
storyAuthor = storyAuthor
|
||||
storyAuthor = storyAuthor,
|
||||
bodyRanges = bodyRanges
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -135,7 +140,7 @@ data class StoryTextPostModel(
|
|||
val useLargeThumbnail = source.text.isBlank()
|
||||
|
||||
view.setTypeface(typeface)
|
||||
view.bindFromStoryTextPost(source.storyTextPost)
|
||||
view.bindFromStoryTextPost(source.storyTextPost, source.bodyRanges)
|
||||
view.bindLinkPreview(linkPreview, useLargeThumbnail, loadThumbnail = false)
|
||||
view.postAdjustLinkPreviewTranslationY()
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.stories
|
|||
import android.content.Context
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.SpannableString
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
|
@ -13,7 +14,9 @@ import androidx.core.view.doOnNextLayout
|
|||
import androidx.core.view.isVisible
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.ClippedCardView
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler
|
||||
import org.thoughtcrime.securesms.conversation.colors.ChatColors
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
|
||||
import org.thoughtcrime.securesms.fonts.TextFont
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
|
@ -119,17 +122,21 @@ class StoryTextPostView @JvmOverloads constructor(
|
|||
postAdjustLinkPreviewTranslationY()
|
||||
}
|
||||
|
||||
fun bindFromStoryTextPost(storyTextPost: StoryTextPost) {
|
||||
fun bindFromStoryTextPost(storyTextPost: StoryTextPost, bodyRanges: BodyRangeList?) {
|
||||
visible = true
|
||||
linkPreviewView.visible = false
|
||||
|
||||
val font = TextFont.fromStyle(storyTextPost.style)
|
||||
val font: TextFont = TextFont.fromStyle(storyTextPost.style)
|
||||
setPostBackground(ChatColors.forChatColor(ChatColors.Id.NotSet, storyTextPost.background).chatBubbleMask)
|
||||
|
||||
if (font.isAllCaps) {
|
||||
setText(storyTextPost.body.uppercase(Locale.getDefault()), false)
|
||||
} else {
|
||||
setText(storyTextPost.body, false)
|
||||
val body = SpannableString(storyTextPost.body)
|
||||
if (font == TextFont.REGULAR && bodyRanges != null) {
|
||||
MessageStyler.style(bodyRanges, body)
|
||||
}
|
||||
setText(body, false)
|
||||
}
|
||||
|
||||
setTextColor(storyTextPost.textForegroundColor, false)
|
||||
|
|
|
@ -132,6 +132,7 @@ class AddToGroupStoryDelegate(
|
|||
.withMedia(result.nonUploadedMedia.toList())
|
||||
.withDraftText(result.body)
|
||||
.withMentions(result.mentions.toList())
|
||||
.withBodyRanges(result.bodyRanges)
|
||||
.build()
|
||||
|
||||
val results = MultiShareSender.sendSync(multiShareArgs)
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.content.res.ColorStateList
|
|||
import android.graphics.drawable.Drawable
|
||||
import android.media.AudioManager
|
||||
import android.os.Bundle
|
||||
import android.text.SpannableString
|
||||
import android.text.method.ScrollingMovementMethod
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
|
@ -46,12 +47,14 @@ import org.thoughtcrime.securesms.contacts.avatars.FallbackPhoto20dp
|
|||
import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto
|
||||
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
|
||||
import org.thoughtcrime.securesms.conversation.ConversationIntents
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler
|
||||
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardBottomSheet
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment
|
||||
import org.thoughtcrime.securesms.mediapreview.VideoControlsDelegate
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
|
@ -803,8 +806,14 @@ class StoryViewerPageFragment :
|
|||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun presentCaption(caption: TextView, largeCaption: TextView, largeCaptionOverlay: View, storyPost: StoryPost) {
|
||||
val displayBody: String = if (storyPost.content is StoryPost.Content.AttachmentContent) {
|
||||
storyPost.content.attachment.caption ?: ""
|
||||
val displayBody: CharSequence = if (storyPost.content is StoryPost.Content.AttachmentContent) {
|
||||
val displayBodySpan = SpannableString(storyPost.content.attachment.caption ?: "")
|
||||
val ranges: BodyRangeList? = storyPost.conversationMessage.messageRecord.messageRanges
|
||||
if (ranges != null && displayBodySpan.isNotEmpty()) {
|
||||
MessageStyler.style(ranges, displayBodySpan)
|
||||
}
|
||||
|
||||
displayBodySpan
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.stories.viewer.post
|
|||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import kotlin.time.Duration
|
||||
|
@ -12,6 +13,7 @@ sealed class StoryPostState {
|
|||
val storyTextPost: StoryTextPost? = null,
|
||||
val linkPreview: LinkPreview? = null,
|
||||
val typeface: Typeface? = null,
|
||||
val bodyRanges: BodyRangeList? = null,
|
||||
val loadState: LoadState = LoadState.INIT
|
||||
) : StoryPostState()
|
||||
|
||||
|
|
|
@ -78,22 +78,22 @@ class StoryPostViewModel(private val repository: StoryTextPostRepository) : View
|
|||
.doOnError { Log.w(TAG, "Failed to get typeface. Rendering with default.", it) }
|
||||
.onErrorReturn { Typeface.DEFAULT }
|
||||
|
||||
val postAndPreviews = repository.getRecord(recordId)
|
||||
.map {
|
||||
if (it.body.isNotEmpty()) {
|
||||
StoryTextPost.parseFrom(Base64.decode(it.body)) to it.linkPreviews.firstOrNull()
|
||||
disposables += Single.zip(typeface, repository.getRecord(recordId), ::Pair).subscribeBy(
|
||||
onSuccess = { (t, record) ->
|
||||
val text: StoryTextPost = if (record.body.isNotEmpty()) {
|
||||
StoryTextPost.parseFrom(Base64.decode(record.body))
|
||||
} else {
|
||||
throw Exception("Text post message body is empty.")
|
||||
}
|
||||
}
|
||||
|
||||
disposables += Single.zip(typeface, postAndPreviews, ::Pair).subscribeBy(
|
||||
onSuccess = { (t, p) ->
|
||||
val linkPreview = record.linkPreviews.firstOrNull()
|
||||
|
||||
store.update {
|
||||
StoryPostState.TextPost(
|
||||
storyTextPost = p.first,
|
||||
linkPreview = p.second,
|
||||
storyTextPost = text,
|
||||
linkPreview = linkPreview,
|
||||
typeface = t,
|
||||
bodyRanges = record.messageRanges,
|
||||
loadState = StoryPostState.LoadState.LOADED
|
||||
)
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ class StoryTextLoader(
|
|||
) {
|
||||
|
||||
fun load() {
|
||||
text.bindFromStoryTextPost(state.storyTextPost!!)
|
||||
text.bindFromStoryTextPost(state.storyTextPost!!, state.bodyRanges)
|
||||
text.bindLinkPreview(state.linkPreview, state.storyTextPost.body.isBlank())
|
||||
text.postAdjustLinkPreviewTranslationY()
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.components.emoji.EmojiToggle
|
|||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
@ -124,13 +125,14 @@ class StoryReplyComposer @JvmOverloads constructor(
|
|||
privacyChrome.visible = true
|
||||
}
|
||||
|
||||
fun consumeInput(): Pair<CharSequence, List<Mention>> {
|
||||
fun consumeInput(): Input {
|
||||
val trimmedText = input.textTrimmed.toString()
|
||||
val mentions = input.mentions
|
||||
val bodyRanges = input.styling
|
||||
|
||||
input.setText("")
|
||||
|
||||
return trimmedText to mentions
|
||||
return Input(trimmedText, mentions, bodyRanges)
|
||||
}
|
||||
|
||||
fun openEmojiSearch() {
|
||||
|
@ -173,4 +175,6 @@ class StoryReplyComposer @JvmOverloads constructor(
|
|||
fun onShowEmojiKeyboard() = Unit
|
||||
fun onHideEmojiKeyboard() = Unit
|
||||
}
|
||||
|
||||
data class Input(val body: String, val mentions: List<Mention>, val bodyRanges: BodyRangeList?)
|
||||
}
|
||||
|
|
|
@ -73,7 +73,8 @@ class StoryDirectReplyDialogFragment :
|
|||
composer = view.findViewById(R.id.input)
|
||||
composer.callback = object : StoryReplyComposer.Callback {
|
||||
override fun onSendActionClicked() {
|
||||
lifecycleDisposable += viewModel.sendReply(composer.consumeInput().first)
|
||||
val (body, _, bodyRanges) = composer.consumeInput()
|
||||
lifecycleDisposable += viewModel.sendReply(body, bodyRanges)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe {
|
||||
Toast.makeText(requireContext(), R.string.StoryDirectReplyDialogFragment__sending_reply, Toast.LENGTH_LONG).show()
|
||||
|
|
|
@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
|||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.ParentStoryId
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
|
@ -26,7 +26,7 @@ class StoryDirectReplyRepository(context: Context) {
|
|||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
fun send(storyId: Long, groupDirectReplyRecipientId: RecipientId?, charSequence: CharSequence, isReaction: Boolean): Completable {
|
||||
fun send(storyId: Long, groupDirectReplyRecipientId: RecipientId?, body: CharSequence, bodyRangeList: BodyRangeList?, isReaction: Boolean): Completable {
|
||||
return Completable.create { emitter ->
|
||||
val message = SignalDatabase.messages.getMessageRecord(storyId) as MediaMmsMessageRecord
|
||||
val (recipient, threadId) = if (groupDirectReplyRecipientId == null) {
|
||||
|
@ -44,24 +44,14 @@ class StoryDirectReplyRepository(context: Context) {
|
|||
MessageSender.send(
|
||||
context,
|
||||
OutgoingMessage(
|
||||
recipient,
|
||||
charSequence.toString(),
|
||||
emptyList(),
|
||||
System.currentTimeMillis(),
|
||||
0,
|
||||
TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong()),
|
||||
false,
|
||||
0,
|
||||
StoryType.NONE,
|
||||
ParentStoryId.DirectReply(storyId),
|
||||
isReaction,
|
||||
QuoteModel(message.dateSent, quoteAuthor.id, message.body, false, message.slideDeck.asAttachments(), null, QuoteModel.Type.NORMAL),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptySet(),
|
||||
emptySet(),
|
||||
null
|
||||
recipient = recipient,
|
||||
body = body.toString(),
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
expiresIn = TimeUnit.SECONDS.toMillis(recipient.expiresInSeconds.toLong()),
|
||||
parentStoryId = ParentStoryId.DirectReply(storyId),
|
||||
isStoryReaction = isReaction,
|
||||
outgoingQuote = QuoteModel(message.dateSent, quoteAuthor.id, message.body, false, message.slideDeck.asAttachments(), null, QuoteModel.Type.NORMAL, message.messageRanges),
|
||||
bodyRanges = bodyRangeList
|
||||
),
|
||||
threadId,
|
||||
MessageSender.SendType.SIGNAL,
|
||||
|
|
|
@ -6,6 +6,7 @@ import androidx.lifecycle.ViewModelProvider
|
|||
import io.reactivex.rxjava3.core.Completable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.livedata.Store
|
||||
|
@ -33,12 +34,24 @@ class StoryDirectReplyViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun sendReply(charSequence: CharSequence): Completable {
|
||||
return repository.send(storyId, groupDirectReplyRecipientId, charSequence, false)
|
||||
fun sendReply(body: CharSequence, bodyRangeList: BodyRangeList?): Completable {
|
||||
return repository.send(
|
||||
storyId = storyId,
|
||||
groupDirectReplyRecipientId = groupDirectReplyRecipientId,
|
||||
body = body,
|
||||
bodyRangeList = bodyRangeList,
|
||||
isReaction = false
|
||||
)
|
||||
}
|
||||
|
||||
fun sendReaction(emoji: CharSequence): Completable {
|
||||
return repository.send(storyId, groupDirectReplyRecipientId, emoji, true)
|
||||
return repository.send(
|
||||
storyId = storyId,
|
||||
groupDirectReplyRecipientId = groupDirectReplyRecipientId,
|
||||
body = emoji,
|
||||
bodyRangeList = null,
|
||||
isReaction = true
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
|
|
|
@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewMod
|
|||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPage
|
||||
|
@ -154,6 +155,7 @@ class StoryGroupReplyFragment :
|
|||
private var resendBody: CharSequence? = null
|
||||
private var resendMentions: List<Mention> = emptyList()
|
||||
private var resendReaction: String? = null
|
||||
private var resendBodyRanges: BodyRangeList? = null
|
||||
|
||||
private lateinit var inlineQueryResultsController: InlineQueryResultsController
|
||||
|
||||
|
@ -349,8 +351,8 @@ class StoryGroupReplyFragment :
|
|||
}
|
||||
|
||||
override fun onSendActionClicked() {
|
||||
val (body, mentions) = composer.consumeInput()
|
||||
performSend(body, mentions)
|
||||
val (body, mentions, bodyRanges) = composer.consumeInput()
|
||||
performSend(body, mentions, bodyRanges)
|
||||
}
|
||||
|
||||
override fun onPickReactionClicked() {
|
||||
|
@ -530,14 +532,15 @@ class StoryGroupReplyFragment :
|
|||
}
|
||||
}
|
||||
|
||||
private fun performSend(body: CharSequence, mentions: List<Mention>) {
|
||||
lifecycleDisposable += StoryGroupReplySender.sendReply(requireContext(), storyId, body, mentions)
|
||||
private fun performSend(body: CharSequence, mentions: List<Mention>, bodyRanges: BodyRangeList?) {
|
||||
lifecycleDisposable += StoryGroupReplySender.sendReply(requireContext(), storyId, body, mentions, bodyRanges)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeBy(
|
||||
onError = { throwable ->
|
||||
if (throwable is UntrustedRecords.UntrustedRecordsException) {
|
||||
resendBody = body
|
||||
resendMentions = mentions
|
||||
resendBodyRanges = bodyRanges
|
||||
|
||||
SafetyNumberBottomSheet
|
||||
.forIdentityRecordsAndDestination(throwable.untrustedRecords, ContactSearchKey.RecipientSearchKey(groupRecipientId, true))
|
||||
|
@ -557,7 +560,7 @@ class StoryGroupReplyFragment :
|
|||
val resendBody = resendBody
|
||||
val resendReaction = resendReaction
|
||||
if (resendBody != null) {
|
||||
performSend(resendBody, resendMentions)
|
||||
performSend(resendBody, resendMentions, resendBodyRanges)
|
||||
} else if (resendReaction != null) {
|
||||
sendReaction(resendReaction)
|
||||
}
|
||||
|
@ -571,6 +574,7 @@ class StoryGroupReplyFragment :
|
|||
resendBody = null
|
||||
resendMentions = emptyList()
|
||||
resendReaction = null
|
||||
resendBodyRanges = null
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
|
|
|
@ -9,7 +9,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
|
|||
import org.thoughtcrime.securesms.database.identity.IdentityRecordList
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.ParentStoryId
|
||||
import org.thoughtcrime.securesms.database.model.StoryType
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.mediasend.v2.UntrustedRecords
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMessage
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
|
@ -19,15 +19,29 @@ import org.thoughtcrime.securesms.sms.MessageSender
|
|||
*/
|
||||
object StoryGroupReplySender {
|
||||
|
||||
fun sendReply(context: Context, storyId: Long, body: CharSequence, mentions: List<Mention>): Completable {
|
||||
return sendInternal(context, storyId, body, mentions, false)
|
||||
fun sendReply(context: Context, storyId: Long, body: CharSequence, mentions: List<Mention>, bodyRanges: BodyRangeList?): Completable {
|
||||
return sendInternal(
|
||||
context = context,
|
||||
storyId = storyId,
|
||||
body = body,
|
||||
mentions = mentions,
|
||||
bodyRanges = bodyRanges,
|
||||
isReaction = false
|
||||
)
|
||||
}
|
||||
|
||||
fun sendReaction(context: Context, storyId: Long, emoji: String): Completable {
|
||||
return sendInternal(context, storyId, emoji, emptyList(), true)
|
||||
return sendInternal(
|
||||
context = context,
|
||||
storyId = storyId,
|
||||
body = emoji,
|
||||
mentions = emptyList(),
|
||||
bodyRanges = null,
|
||||
isReaction = true
|
||||
)
|
||||
}
|
||||
|
||||
private fun sendInternal(context: Context, storyId: Long, body: CharSequence, mentions: List<Mention>, isReaction: Boolean): Completable {
|
||||
private fun sendInternal(context: Context, storyId: Long, body: CharSequence, mentions: List<Mention>, bodyRanges: BodyRangeList?, isReaction: Boolean): Completable {
|
||||
val messageAndRecipient = Single.fromCallable {
|
||||
val message = SignalDatabase.messages.getMessageRecord(storyId)
|
||||
val recipient = SignalDatabase.threads.getRecipientForThreadId(message.threadId)!!
|
||||
|
@ -44,13 +58,12 @@ object StoryGroupReplySender {
|
|||
OutgoingMessage(
|
||||
recipient = recipient,
|
||||
body = body.toString(),
|
||||
timestamp = System.currentTimeMillis(),
|
||||
distributionType = 0,
|
||||
storyType = StoryType.NONE,
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
parentStoryId = ParentStoryId.GroupReply(message.id),
|
||||
isStoryReaction = isReaction,
|
||||
mentions = mentions,
|
||||
isSecure = true
|
||||
isSecure = true,
|
||||
bodyRanges = bodyRanges
|
||||
),
|
||||
message.threadId,
|
||||
MessageSender.SendType.SIGNAL,
|
||||
|
|
|
@ -104,6 +104,7 @@ public final class FeatureFlags {
|
|||
private static final String CHAT_FILTERS = "android.chat.filters.3";
|
||||
private static final String PAYPAL_ONE_TIME_DONATIONS = "android.oneTimePayPalDonations.2";
|
||||
private static final String PAYPAL_RECURRING_DONATIONS = "android.recurringPayPalDonations.2";
|
||||
private static final String TEXT_FORMATTING = "android.textFormatting";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
|
@ -158,7 +159,8 @@ public final class FeatureFlags {
|
|||
CDS_HARD_LIMIT,
|
||||
CHAT_FILTERS,
|
||||
PAYPAL_ONE_TIME_DONATIONS,
|
||||
PAYPAL_RECURRING_DONATIONS
|
||||
PAYPAL_RECURRING_DONATIONS,
|
||||
TEXT_FORMATTING
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -220,7 +222,8 @@ public final class FeatureFlags {
|
|||
RECIPIENT_MERGE_V2,
|
||||
CREDIT_CARD_PAYMENTS,
|
||||
PAYMENTS_REQUEST_ACTIVATE_FLOW,
|
||||
CDS_HARD_LIMIT
|
||||
CDS_HARD_LIMIT,
|
||||
TEXT_FORMATTING
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -564,6 +567,13 @@ public final class FeatureFlags {
|
|||
return getBoolean(PAYPAL_RECURRING_DONATIONS, Environment.IS_STAGING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not we should show text formatting options.
|
||||
*/
|
||||
public static boolean textFormatting() {
|
||||
return getBoolean(TEXT_FORMATTING, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
|
@ -10,6 +10,7 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.StringUtil;
|
||||
import org.signal.libsignal.protocol.util.Pair;
|
||||
|
||||
import java.security.InvalidParameterException;
|
||||
|
@ -25,7 +26,7 @@ public class SearchUtil {
|
|||
|
||||
public static Spannable getHighlightedSpan(@NonNull Locale locale,
|
||||
@NonNull StyleFactory styleFactory,
|
||||
@Nullable String text,
|
||||
@Nullable CharSequence text,
|
||||
@Nullable String highlight,
|
||||
int matchMode)
|
||||
{
|
||||
|
@ -33,9 +34,9 @@ public class SearchUtil {
|
|||
return new SpannableString("");
|
||||
}
|
||||
|
||||
text = text.replaceAll("\n", " ");
|
||||
text = StringUtil.replace(text, '\n', " ");
|
||||
|
||||
return getHighlightedSpan(locale, styleFactory, new SpannableString(text), highlight, matchMode);
|
||||
return getHighlightedSpan(locale, styleFactory, SpannableString.valueOf(text), highlight, matchMode);
|
||||
}
|
||||
|
||||
public static Spannable getHighlightedSpan(@NonNull Locale locale,
|
||||
|
@ -53,7 +54,7 @@ public class SearchUtil {
|
|||
return text;
|
||||
}
|
||||
|
||||
SpannableString spanned = new SpannableString(text);
|
||||
SpannableString spanned = SpannableString.valueOf(text);
|
||||
List<Pair<Integer, Integer>> ranges;
|
||||
|
||||
switch (matchMode) {
|
||||
|
@ -67,8 +68,11 @@ public class SearchUtil {
|
|||
throw new InvalidParameterException("match mode must be STRICT or MATCH_ALL: " + matchMode);
|
||||
}
|
||||
|
||||
CharacterStyle[] styles = styleFactory.createStyles();
|
||||
for (Pair<Integer, Integer> range : ranges) {
|
||||
spanned.setSpan(styleFactory.create(), range.first(), range.second(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
for (CharacterStyle style : styles) {
|
||||
spanned.setSpan(style, range.first(), range.second(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
return spanned;
|
||||
|
@ -148,6 +152,6 @@ public class SearchUtil {
|
|||
}
|
||||
|
||||
public interface StyleFactory {
|
||||
CharacterStyle create();
|
||||
CharacterStyle[] createStyles();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -77,8 +77,11 @@ message ProfileChangeDetails {
|
|||
message BodyRangeList {
|
||||
message BodyRange {
|
||||
enum Style {
|
||||
BOLD = 0;
|
||||
ITALIC = 1;
|
||||
BOLD = 0;
|
||||
ITALIC = 1;
|
||||
SPOILER = 2;
|
||||
STRIKETHROUGH = 3;
|
||||
MONOSPACE = 4;
|
||||
}
|
||||
|
||||
message Button {
|
||||
|
|
|
@ -22,4 +22,9 @@
|
|||
<item name="navigation_bar_guideline" type="id" />
|
||||
|
||||
<item name="reaction_bar" type="id" />
|
||||
|
||||
<item name="edittext_bold" type="id" />
|
||||
<item name="edittext_italic" type="id" />
|
||||
<item name="edittext_strikethrough" type="id" />
|
||||
<item name="edittext_monospace" type="id" />
|
||||
</resources>
|
||||
|
|
|
@ -5510,4 +5510,13 @@
|
|||
<!-- Button label on sms removal info/megaphone to start the export SMS flow -->
|
||||
<string name="SmsRemoval_export_sms">Export SMS</string>
|
||||
|
||||
<!-- Text Formatting -->
|
||||
<!-- Popup menu label for applying bold style -->
|
||||
<string name="TextFormatting_bold">Bold</string>
|
||||
<!-- Popup menu label for applying italic style -->
|
||||
<string name="TextFormatting_italic">Italic</string>
|
||||
<!-- Popup menu label for applying strikethrough style -->
|
||||
<string name="TextFormatting_strikethrough">Strikethrough</string>
|
||||
<!-- Popup menu label for applying monospace font style -->
|
||||
<string name="TextFormatting_monospace">Monospace</string>
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
|
||||
class BodyRangeUtilTest {
|
||||
|
||||
@Test
|
||||
fun testMentionBeforeBodyRange() {
|
||||
val bodyRangeList = BodyRangeList.newBuilder().addRanges(BodyRangeList.BodyRange.newBuilder().setStart(5).setLength(5).build()).build()
|
||||
val adjustments = listOf(BodyAdjustment(0, 3, 1))
|
||||
|
||||
val updatedBodyRanges = bodyRangeList.adjustBodyRanges(adjustments)!!
|
||||
|
||||
assertEquals(3, updatedBodyRanges.getRanges(0).start)
|
||||
assertEquals(5, updatedBodyRanges.getRanges(0).length)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun textMentionAfterBodyRange() {
|
||||
val bodyRangeList = BodyRangeList.newBuilder().addRanges(BodyRangeList.BodyRange.newBuilder().setStart(5).setLength(5).build()).build()
|
||||
val adjustments = listOf(BodyAdjustment(10, 3, 1))
|
||||
|
||||
val updatedBodyRanges = bodyRangeList.adjustBodyRanges(adjustments)!!
|
||||
|
||||
assertEquals(5, updatedBodyRanges.getRanges(0).start)
|
||||
assertEquals(5, updatedBodyRanges.getRanges(0).length)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMentionWithinBodyRange() {
|
||||
val bodyRangeList = BodyRangeList.newBuilder().addRanges(BodyRangeList.BodyRange.newBuilder().setStart(0).setLength(20).build()).build()
|
||||
val adjustments = listOf(BodyAdjustment(5, 10, 1))
|
||||
|
||||
val updatedBodyRanges = bodyRangeList.adjustBodyRanges(adjustments)!!
|
||||
|
||||
assertEquals(0, updatedBodyRanges.getRanges(0).start)
|
||||
assertEquals(11, updatedBodyRanges.getRanges(0).length)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testMentionWithinAndEndOfBodyRange() {
|
||||
val bodyRangeList = BodyRangeList.newBuilder().addRanges(BodyRangeList.BodyRange.newBuilder().setStart(0).setLength(5).build()).build()
|
||||
val adjustments = listOf(BodyAdjustment(1, 4, 1))
|
||||
|
||||
val updatedBodyRanges = bodyRangeList.adjustBodyRanges(adjustments)!!
|
||||
|
||||
assertEquals(0, updatedBodyRanges.getRanges(0).start)
|
||||
assertEquals(2, updatedBodyRanges.getRanges(0).length)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDoubleMention() {
|
||||
val bodyRangeList = BodyRangeList.newBuilder().addRanges(BodyRangeList.BodyRange.newBuilder().setStart(5).setLength(10).build()).build()
|
||||
val adjustments = listOf(BodyAdjustment(0, 3, 1), BodyAdjustment(17, 10, 1))
|
||||
|
||||
val updatedBodyRanges = bodyRangeList.adjustBodyRanges(adjustments)!!
|
||||
|
||||
assertEquals(3, updatedBodyRanges.getRanges(0).start)
|
||||
assertEquals(10, updatedBodyRanges.getRanges(0).length)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testResolvedMentionBeforeBodyRange() {
|
||||
val bodyRangeList = BodyRangeList.newBuilder().addRanges(BodyRangeList.BodyRange.newBuilder().setStart(10).setLength(20).build()).build()
|
||||
val adjustments = listOf(BodyAdjustment(0, 1, 10))
|
||||
|
||||
val updatedBodyRanges = bodyRangeList.adjustBodyRanges(adjustments)!!
|
||||
|
||||
assertEquals(19, updatedBodyRanges.getRanges(0).start)
|
||||
assertEquals(20, updatedBodyRanges.getRanges(0).length)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun textResolvedMentionAfterBodyRange() {
|
||||
val bodyRangeList = BodyRangeList.newBuilder().addRanges(BodyRangeList.BodyRange.newBuilder().setStart(5).setLength(5).build()).build()
|
||||
val adjustments = listOf(BodyAdjustment(10, 1, 10))
|
||||
|
||||
val updatedBodyRanges = bodyRangeList.adjustBodyRanges(adjustments)!!
|
||||
|
||||
assertEquals(5, updatedBodyRanges.getRanges(0).start)
|
||||
assertEquals(5, updatedBodyRanges.getRanges(0).length)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testResolvedMentionWithinBodyRange() {
|
||||
val bodyRangeList = BodyRangeList.newBuilder().addRanges(BodyRangeList.BodyRange.newBuilder().setStart(0).setLength(20).build()).build()
|
||||
val adjustments = listOf(BodyAdjustment(5, 1, 11))
|
||||
|
||||
val updatedBodyRanges = bodyRangeList.adjustBodyRanges(adjustments)!!
|
||||
|
||||
assertEquals(0, updatedBodyRanges.getRanges(0).start)
|
||||
assertEquals(30, updatedBodyRanges.getRanges(0).length)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testResolvedMentionWithinAndEndOfBodyRange() {
|
||||
val bodyRangeList = BodyRangeList.newBuilder().addRanges(BodyRangeList.BodyRange.newBuilder().setStart(0).setLength(2).build()).build()
|
||||
val adjustments = listOf(BodyAdjustment(1, 1, 4))
|
||||
|
||||
val updatedBodyRanges = bodyRangeList.adjustBodyRanges(adjustments)!!
|
||||
|
||||
assertEquals(0, updatedBodyRanges.getRanges(0).start)
|
||||
assertEquals(5, updatedBodyRanges.getRanges(0).length)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testDoubleResolvedMention() {
|
||||
val bodyRangeList = BodyRangeList.newBuilder().addRanges(BodyRangeList.BodyRange.newBuilder().setStart(2).setLength(4).build()).build()
|
||||
val adjustments = listOf(BodyAdjustment(0, 1, 8), BodyAdjustment(7, 1, 11))
|
||||
|
||||
val updatedBodyRanges = bodyRangeList.adjustBodyRanges(adjustments)!!
|
||||
|
||||
assertEquals(9, updatedBodyRanges.getRanges(0).start)
|
||||
assertEquals(4, updatedBodyRanges.getRanges(0).length)
|
||||
}
|
||||
}
|
|
@ -20,7 +20,7 @@ class MentionUtilTest {
|
|||
|
||||
val update: MentionUtil.UpdatedBodyAndMentions = MentionUtil.update("T test", mentions) { it.recipientId.toString() }
|
||||
|
||||
assertThat(update.body, Matchers.`is`("RecipientId::1 test"))
|
||||
assertThat(update.body.toString(), Matchers.`is`("RecipientId::1 test"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -32,7 +32,7 @@ class MentionUtilTest {
|
|||
|
||||
val update: MentionUtil.UpdatedBodyAndMentions = MentionUtil.update("ONETWO test", mentions) { it.recipientId.toString() }
|
||||
|
||||
assertThat(update.body, Matchers.`is`("RecipientId::1RecipientId::2 test"))
|
||||
assertThat(update.body.toString(), Matchers.`is`("RecipientId::1RecipientId::2 test"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -44,6 +44,6 @@ class MentionUtilTest {
|
|||
|
||||
val update: MentionUtil.UpdatedBodyAndMentions = MentionUtil.update("T test", mentions) { it.recipientId.toString() }
|
||||
|
||||
assertThat(update.body, Matchers.`is`("RecipientId::1est"))
|
||||
assertThat(update.body.toString(), Matchers.`is`("RecipientId::1est"))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
package org.signal.core.util;
|
||||
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -326,4 +328,52 @@ public final class StringUtil {
|
|||
public static String forceLtr(@NonNull CharSequence text) {
|
||||
return "\u202a" + text + "\u202c";
|
||||
}
|
||||
|
||||
public static @NonNull CharSequence replace(@NonNull CharSequence text, char toReplace, String replacement) {
|
||||
SpannableStringBuilder updatedText = null;
|
||||
|
||||
for (int i = text.length() - 1; i >= 0; i--) {
|
||||
if (text.charAt(i) == toReplace) {
|
||||
if (updatedText == null) {
|
||||
updatedText = SpannableStringBuilder.valueOf(text);
|
||||
}
|
||||
updatedText.replace(i, i + 1, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedText != null) {
|
||||
return updatedText;
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean startsWith(@NonNull CharSequence text, @NonNull CharSequence substring) {
|
||||
if (substring.length() > text.length()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < substring.length(); i++) {
|
||||
if (text.charAt(i) != substring.charAt(i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean endsWith(@NonNull CharSequence text, @NonNull CharSequence substring) {
|
||||
if (substring.length() > text.length()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int textIndex = text.length() - 1;
|
||||
for (int substringIndex = substring.length() - 1; substringIndex >= 0; substringIndex--, textIndex--) {
|
||||
if (text.charAt(textIndex) != substring.charAt(substringIndex)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
|
||||
package org.signal.core.util
|
||||
|
||||
import android.app.Application
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters
|
||||
import org.robolectric.annotation.Config
|
||||
import java.lang.Boolean as JavaBoolean
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(value = ParameterizedRobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class StringUtilTest_endsWith {
|
||||
|
||||
@Parameter(0)
|
||||
lateinit var text: CharSequence
|
||||
|
||||
@Parameter(1)
|
||||
lateinit var substring: CharSequence
|
||||
|
||||
@Parameter(2)
|
||||
lateinit var expected: JavaBoolean
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameters
|
||||
fun data(): Collection<Array<Any>> {
|
||||
return listOf(
|
||||
arrayOf("Text", "xt", true),
|
||||
arrayOf("Text", "", true),
|
||||
arrayOf("Text", "XT", false),
|
||||
arrayOf("Text…", "xt…", true),
|
||||
arrayOf("", "Te", false),
|
||||
arrayOf("Text", "Text", true),
|
||||
arrayOf("Text", "2Text", false),
|
||||
arrayOf("\uD83D\uDC64Text", "Te", false),
|
||||
arrayOf("Text text text\uD83D\uDC64", "\uD83D\uDC64", true),
|
||||
arrayOf("Text\uD83D\uDC64Text", "\uD83D\uDC64Text", true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun replace() {
|
||||
val result = StringUtil.endsWith(text, substring)
|
||||
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package org.signal.core.util
|
||||
|
||||
import android.app.Application
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters
|
||||
import org.robolectric.annotation.Config
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(value = ParameterizedRobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class StringUtilTest_replace {
|
||||
|
||||
@Parameter(0)
|
||||
lateinit var text: CharSequence
|
||||
|
||||
@Parameter(1)
|
||||
lateinit var charToReplace: Character
|
||||
|
||||
@Parameter(2)
|
||||
lateinit var replacement: String
|
||||
|
||||
@Parameter(3)
|
||||
lateinit var expected: CharSequence
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameters
|
||||
fun data(): Collection<Array<Any>> {
|
||||
return listOf(
|
||||
arrayOf("Replace\nme", '\n', " ", "Replace me"),
|
||||
arrayOf("Replace me", '\n', " ", "Replace me"),
|
||||
arrayOf("\nReplace me", '\n', " ", " Replace me"),
|
||||
arrayOf("Replace me\n", '\n', " ", "Replace me "),
|
||||
arrayOf("Replace\n\nme", '\n', " ", "Replace me"),
|
||||
arrayOf("Replace\nme\n", '\n', " ", "Replace me "),
|
||||
arrayOf("\n\nReplace\n\nme\n", '\n', " ", " Replace me ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun replace() {
|
||||
val result = StringUtil.replace(text, charToReplace.charValue(), replacement)
|
||||
|
||||
assertEquals(expected.toString(), result.toString())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
@file:Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
|
||||
package org.signal.core.util
|
||||
|
||||
import android.app.Application
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner.Parameter
|
||||
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters
|
||||
import org.robolectric.annotation.Config
|
||||
import java.lang.Boolean as JavaBoolean
|
||||
|
||||
@Suppress("ClassName")
|
||||
@RunWith(value = ParameterizedRobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class StringUtilTest_startsWith {
|
||||
|
||||
@Parameter(0)
|
||||
lateinit var text: CharSequence
|
||||
|
||||
@Parameter(1)
|
||||
lateinit var substring: CharSequence
|
||||
|
||||
@Parameter(2)
|
||||
lateinit var expected: JavaBoolean
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameters
|
||||
fun data(): Collection<Array<Any>> {
|
||||
return listOf(
|
||||
arrayOf("Text", "Te", true),
|
||||
arrayOf("Text", "", true),
|
||||
arrayOf("Text", "te", false),
|
||||
arrayOf("…Text", "…Te", true),
|
||||
arrayOf("", "Te", false),
|
||||
arrayOf("Text", "Text", true),
|
||||
arrayOf("Text", "Text2", false),
|
||||
arrayOf("\uD83D\uDC64Text", "Te", false),
|
||||
arrayOf("Text text text\uD83D\uDC64", "\uD83D\uDC64", false),
|
||||
arrayOf("\uD83D\uDC64Text", "\uD83D\uDC64Te", true)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun replace() {
|
||||
val result = StringUtil.startsWith(text, substring)
|
||||
|
||||
assertEquals(expected, result)
|
||||
}
|
||||
}
|
|
@ -101,6 +101,7 @@ import org.whispersystems.signalservice.internal.push.SendGroupMessageResponse;
|
|||
import org.whispersystems.signalservice.internal.push.SendMessageResponse;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.AttachmentPointer;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.BodyRange;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.CallMessage;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.Content;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage;
|
||||
|
@ -845,6 +846,10 @@ public class SignalServiceMessageSender {
|
|||
builder.setTextAttachment(createTextAttachment(message.getTextAttachment().get()));
|
||||
}
|
||||
|
||||
if (message.getBodyRanges().isPresent()) {
|
||||
builder.addAllBodyRanges(message.getBodyRanges().get());
|
||||
}
|
||||
|
||||
builder.setAllowsReplies(message.getAllowsReplies().orElse(true));
|
||||
|
||||
return container.setStoryMessage(builder).build();
|
||||
|
@ -929,15 +934,20 @@ public class SignalServiceMessageSender {
|
|||
List<SignalServiceDataMessage.Mention> mentions = message.getQuote().get().getMentions();
|
||||
if (mentions != null && !mentions.isEmpty()) {
|
||||
for (SignalServiceDataMessage.Mention mention : mentions) {
|
||||
quoteBuilder.addBodyRanges(DataMessage.BodyRange.newBuilder()
|
||||
.setStart(mention.getStart())
|
||||
.setLength(mention.getLength())
|
||||
.setMentionUuid(mention.getServiceId().toString()));
|
||||
quoteBuilder.addBodyRanges(BodyRange.newBuilder()
|
||||
.setStart(mention.getStart())
|
||||
.setLength(mention.getLength())
|
||||
.setMentionUuid(mention.getServiceId().toString()));
|
||||
}
|
||||
|
||||
builder.setRequiredProtocolVersion(Math.max(DataMessage.ProtocolVersion.MENTIONS_VALUE, builder.getRequiredProtocolVersion()));
|
||||
}
|
||||
|
||||
List<BodyRange> bodyRanges = message.getQuote().get().getBodyRanges();
|
||||
if (bodyRanges != null) {
|
||||
quoteBuilder.addAllBodyRanges(bodyRanges);
|
||||
}
|
||||
|
||||
List<SignalServiceDataMessage.Quote.QuotedAttachment> attachments = message.getQuote().get().getAttachments();
|
||||
if (attachments != null) {
|
||||
for (SignalServiceDataMessage.Quote.QuotedAttachment attachment : attachments) {
|
||||
|
@ -972,10 +982,10 @@ 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.getServiceId().toString()));
|
||||
builder.addBodyRanges(BodyRange.newBuilder()
|
||||
.setStart(mention.getStart())
|
||||
.setLength(mention.getLength())
|
||||
.setMentionUuid(mention.getServiceId().toString()));
|
||||
}
|
||||
builder.setRequiredProtocolVersion(Math.max(DataMessage.ProtocolVersion.MENTIONS_VALUE, builder.getRequiredProtocolVersion()));
|
||||
}
|
||||
|
@ -1060,6 +1070,10 @@ public class SignalServiceMessageSender {
|
|||
.setReceiptCredentialPresentation(ByteString.copyFrom(giftBadge.getReceiptCredentialPresentation().serialize())));
|
||||
}
|
||||
|
||||
if (message.getBodyRanges().isPresent()) {
|
||||
builder.addAllBodyRanges(message.getBodyRanges().get());
|
||||
}
|
||||
|
||||
builder.setTimestamp(message.getTimestamp());
|
||||
|
||||
return enforceMaxContentSize(container.setDataMessage(builder).build());
|
||||
|
|
|
@ -680,9 +680,9 @@ import javax.annotation.Nullable;
|
|||
Optional<SignalServiceGroupV2> groupContext = Optional.ofNullable(groupInfoV2);
|
||||
|
||||
List<SignalServiceAttachment> attachments = new LinkedList<>();
|
||||
boolean endSession = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.END_SESSION_VALUE ) != 0);
|
||||
boolean endSession = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.END_SESSION_VALUE) != 0);
|
||||
boolean expirationUpdate = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.EXPIRATION_TIMER_UPDATE_VALUE) != 0);
|
||||
boolean profileKeyUpdate = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.PROFILE_KEY_UPDATE_VALUE ) != 0);
|
||||
boolean profileKeyUpdate = ((content.getFlags() & SignalServiceProtos.DataMessage.Flags.PROFILE_KEY_UPDATE_VALUE) != 0);
|
||||
boolean isGroupV2 = groupInfoV2 != null;
|
||||
SignalServiceDataMessage.Quote quote = createQuote(content, isGroupV2);
|
||||
List<SharedContact> sharedContacts = createSharedContacts(content);
|
||||
|
@ -694,6 +694,7 @@ import javax.annotation.Nullable;
|
|||
SignalServiceDataMessage.GroupCallUpdate groupCallUpdate = createGroupCallUpdate(content);
|
||||
SignalServiceDataMessage.StoryContext storyContext = createStoryContext(content);
|
||||
SignalServiceDataMessage.GiftBadge giftBadge = createGiftBadge(content);
|
||||
List<SignalServiceProtos.BodyRange> bodyRanges = createBodyRanges(content.getBodyRangesList(), content.getBody());
|
||||
|
||||
if (content.getRequiredProtocolVersion() > SignalServiceProtos.DataMessage.ProtocolVersion.CURRENT_VALUE) {
|
||||
throw new UnsupportedDataMessageProtocolVersionException(SignalServiceProtos.DataMessage.ProtocolVersion.CURRENT_VALUE,
|
||||
|
@ -745,6 +746,7 @@ import javax.annotation.Nullable;
|
|||
.withPayment(payment)
|
||||
.withStoryContext(storyContext)
|
||||
.withGiftBadge(giftBadge)
|
||||
.withBodyRanges(bodyRanges)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -1092,12 +1094,14 @@ import javax.annotation.Nullable;
|
|||
return SignalServiceStoryMessage.forFileAttachment(profileKey,
|
||||
createGroupV2Info(content),
|
||||
createAttachmentPointer(content.getFileAttachment()),
|
||||
content.getAllowsReplies());
|
||||
content.getAllowsReplies(),
|
||||
content.getBodyRangesList());
|
||||
} else {
|
||||
return SignalServiceStoryMessage.forTextAttachment(profileKey,
|
||||
createGroupV2Info(content),
|
||||
createTextAttachment(content.getTextAttachment()),
|
||||
content.getAllowsReplies());
|
||||
content.getAllowsReplies(),
|
||||
content.getBodyRangesList());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1121,7 +1125,8 @@ import javax.annotation.Nullable;
|
|||
content.getQuote().getText(),
|
||||
attachments,
|
||||
createMentions(content.getQuote().getBodyRangesList(), content.getQuote().getText(), isGroupV2),
|
||||
SignalServiceDataMessage.Quote.Type.fromProto(content.getQuote().getType()));
|
||||
SignalServiceDataMessage.Quote.Type.fromProto(content.getQuote().getType()),
|
||||
createBodyRanges(content.getQuote().getBodyRangesList(), content.getQuote().getText()));
|
||||
} else {
|
||||
Log.w(TAG, "Quote was missing an author! Returning null.");
|
||||
return null;
|
||||
|
@ -1154,7 +1159,7 @@ import javax.annotation.Nullable;
|
|||
Optional.ofNullable(attachment));
|
||||
}
|
||||
|
||||
private static @Nullable List<SignalServiceDataMessage.Mention> createMentions(List<SignalServiceProtos.DataMessage.BodyRange> bodyRanges, String body, boolean isGroupV2)
|
||||
private static @Nullable List<SignalServiceDataMessage.Mention> createMentions(List<SignalServiceProtos.BodyRange> bodyRanges, String body, boolean isGroupV2)
|
||||
throws InvalidMessageStructureException
|
||||
{
|
||||
if (bodyRanges == null || bodyRanges.isEmpty() || body == null) {
|
||||
|
@ -1163,7 +1168,7 @@ import javax.annotation.Nullable;
|
|||
|
||||
List<SignalServiceDataMessage.Mention> mentions = new LinkedList<>();
|
||||
|
||||
for (SignalServiceProtos.DataMessage.BodyRange bodyRange : bodyRanges) {
|
||||
for (SignalServiceProtos.BodyRange bodyRange : bodyRanges) {
|
||||
if (bodyRange.hasMentionUuid()) {
|
||||
try {
|
||||
mentions.add(new SignalServiceDataMessage.Mention(ServiceId.parseOrThrow(bodyRange.getMentionUuid()), bodyRange.getStart(), bodyRange.getLength()));
|
||||
|
@ -1180,6 +1185,22 @@ import javax.annotation.Nullable;
|
|||
return mentions;
|
||||
}
|
||||
|
||||
private static @Nullable List<SignalServiceProtos.BodyRange> createBodyRanges(List<SignalServiceProtos.BodyRange> bodyRanges, String body) {
|
||||
if (bodyRanges == null || bodyRanges.isEmpty() || body == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<SignalServiceProtos.BodyRange> ranges = new LinkedList<>();
|
||||
|
||||
for (SignalServiceProtos.BodyRange bodyRange : bodyRanges) {
|
||||
if (bodyRange.hasStyle()) {
|
||||
ranges.add(bodyRange);
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
private static @Nullable SignalServiceDataMessage.Sticker createSticker(SignalServiceProtos.DataMessage content) throws InvalidMessageStructureException {
|
||||
if (!content.hasSticker() ||
|
||||
!content.getSticker().hasPackId() ||
|
||||
|
|
|
@ -11,6 +11,7 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact
|
|||
import org.whispersystems.signalservice.api.push.ServiceId
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil.asOptional
|
||||
import org.whispersystems.signalservice.api.util.OptionalUtil.emptyIfStringEmpty
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.BodyRange
|
||||
import java.util.LinkedList
|
||||
import java.util.Optional
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage.Payment as PaymentProto
|
||||
|
@ -47,7 +48,8 @@ class SignalServiceDataMessage private constructor(
|
|||
val groupCallUpdate: Optional<GroupCallUpdate>,
|
||||
val payment: Optional<Payment>,
|
||||
val storyContext: Optional<StoryContext>,
|
||||
val giftBadge: Optional<GiftBadge>
|
||||
val giftBadge: Optional<GiftBadge>,
|
||||
val bodyRanges: Optional<List<BodyRange>>
|
||||
) {
|
||||
val isActivatePaymentsRequest: Boolean = payment.map { it.isActivationRequest }.orElse(false)
|
||||
val isPaymentsActivated: Boolean = payment.map { it.isActivation }.orElse(false)
|
||||
|
@ -92,6 +94,7 @@ class SignalServiceDataMessage private constructor(
|
|||
private var payment: Payment? = null
|
||||
private var storyContext: StoryContext? = null
|
||||
private var giftBadge: GiftBadge? = null
|
||||
private var bodyRanges: MutableList<BodyRange> = LinkedList<BodyRange>()
|
||||
|
||||
fun withTimestamp(timestamp: Long): Builder {
|
||||
this.timestamp = timestamp
|
||||
|
@ -210,6 +213,11 @@ class SignalServiceDataMessage private constructor(
|
|||
return this
|
||||
}
|
||||
|
||||
fun withBodyRanges(bodyRanges: List<BodyRange>?): Builder {
|
||||
bodyRanges?.let { this.bodyRanges.addAll(bodyRanges) }
|
||||
return this
|
||||
}
|
||||
|
||||
fun build(): SignalServiceDataMessage {
|
||||
if (timestamp == 0L) {
|
||||
timestamp = System.currentTimeMillis()
|
||||
|
@ -236,7 +244,8 @@ class SignalServiceDataMessage private constructor(
|
|||
groupCallUpdate = groupCallUpdate.asOptional(),
|
||||
payment = payment.asOptional(),
|
||||
storyContext = storyContext.asOptional(),
|
||||
giftBadge = giftBadge.asOptional()
|
||||
giftBadge = giftBadge.asOptional(),
|
||||
bodyRanges = bodyRanges.asOptional()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -247,7 +256,8 @@ class SignalServiceDataMessage private constructor(
|
|||
val text: String,
|
||||
val attachments: List<QuotedAttachment>?,
|
||||
val mentions: List<Mention>?,
|
||||
val type: Type
|
||||
val type: Type,
|
||||
val bodyRanges: List<BodyRange>?
|
||||
) {
|
||||
enum class Type(val protoType: QuoteProto.Type) {
|
||||
NORMAL(QuoteProto.Type.NORMAL),
|
||||
|
|
|
@ -1,39 +1,50 @@
|
|||
package org.whispersystems.signalservice.api.messages;
|
||||
|
||||
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public class SignalServiceStoryMessage {
|
||||
private final Optional<byte[]> profileKey;
|
||||
private final Optional<SignalServiceGroupV2> groupContext;
|
||||
private final Optional<SignalServiceAttachment> fileAttachment;
|
||||
private final Optional<SignalServiceTextAttachment> textAttachment;
|
||||
private final Optional<Boolean> allowsReplies;
|
||||
private final Optional<byte[]> profileKey;
|
||||
private final Optional<SignalServiceGroupV2> groupContext;
|
||||
private final Optional<SignalServiceAttachment> fileAttachment;
|
||||
private final Optional<SignalServiceTextAttachment> textAttachment;
|
||||
private final Optional<Boolean> allowsReplies;
|
||||
private final Optional<List<SignalServiceProtos.BodyRange>> bodyRanges;
|
||||
|
||||
private SignalServiceStoryMessage(byte[] profileKey,
|
||||
SignalServiceGroupV2 groupContext,
|
||||
SignalServiceAttachment fileAttachment,
|
||||
SignalServiceTextAttachment textAttachment,
|
||||
boolean allowsReplies) {
|
||||
boolean allowsReplies,
|
||||
List<SignalServiceProtos.BodyRange> bodyRanges)
|
||||
{
|
||||
this.profileKey = Optional.ofNullable(profileKey);
|
||||
this.groupContext = Optional.ofNullable(groupContext);
|
||||
this.fileAttachment = Optional.ofNullable(fileAttachment);
|
||||
this.textAttachment = Optional.ofNullable(textAttachment);
|
||||
this.allowsReplies = Optional.of(allowsReplies);
|
||||
this.bodyRanges = Optional.ofNullable(bodyRanges);
|
||||
}
|
||||
|
||||
public static SignalServiceStoryMessage forFileAttachment(byte[] profileKey,
|
||||
SignalServiceGroupV2 groupContext,
|
||||
SignalServiceAttachment fileAttachment,
|
||||
boolean allowsReplies) {
|
||||
return new SignalServiceStoryMessage(profileKey, groupContext, fileAttachment, null, allowsReplies);
|
||||
boolean allowsReplies,
|
||||
List<SignalServiceProtos.BodyRange> bodyRanges)
|
||||
{
|
||||
return new SignalServiceStoryMessage(profileKey, groupContext, fileAttachment, null, allowsReplies, bodyRanges);
|
||||
}
|
||||
|
||||
public static SignalServiceStoryMessage forTextAttachment(byte[] profileKey,
|
||||
SignalServiceGroupV2 groupContext,
|
||||
SignalServiceTextAttachment textAttachment,
|
||||
boolean allowsReplies) {
|
||||
return new SignalServiceStoryMessage(profileKey, groupContext, null, textAttachment, allowsReplies);
|
||||
boolean allowsReplies,
|
||||
List<SignalServiceProtos.BodyRange> bodyRanges)
|
||||
{
|
||||
return new SignalServiceStoryMessage(profileKey, groupContext, null, textAttachment, allowsReplies, bodyRanges);
|
||||
}
|
||||
|
||||
public Optional<byte[]> getProfileKey() {
|
||||
|
@ -55,4 +66,8 @@ public class SignalServiceStoryMessage {
|
|||
public Optional<Boolean> getAllowsReplies() {
|
||||
return allowsReplies;
|
||||
}
|
||||
|
||||
public Optional<List<SignalServiceProtos.BodyRange>> getBodyRanges() {
|
||||
return bodyRanges;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -125,6 +125,25 @@ message CallMessage {
|
|||
optional Opaque opaque = 10;
|
||||
}
|
||||
|
||||
message BodyRange {
|
||||
enum Style {
|
||||
NONE = 0;
|
||||
BOLD = 1;
|
||||
ITALIC = 2;
|
||||
SPOILER = 3;
|
||||
STRIKETHROUGH = 4;
|
||||
MONOSPACE = 5;
|
||||
}
|
||||
|
||||
optional uint32 start = 1;
|
||||
optional uint32 length = 2;
|
||||
|
||||
oneof associatedValue {
|
||||
string mentionUuid = 3;
|
||||
Style style = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message DataMessage {
|
||||
enum Flags {
|
||||
END_SESSION = 1;
|
||||
|
@ -132,15 +151,6 @@ message DataMessage {
|
|||
PROFILE_KEY_UPDATE = 4;
|
||||
}
|
||||
|
||||
message BodyRange {
|
||||
optional uint32 start = 1;
|
||||
optional uint32 length = 2;
|
||||
|
||||
oneof associatedValue {
|
||||
string mentionUuid = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message Quote {
|
||||
enum Type {
|
||||
NORMAL = 0;
|
||||
|
@ -382,6 +392,7 @@ message StoryMessage {
|
|||
TextAttachment textAttachment = 4;
|
||||
}
|
||||
optional bool allowsReplies = 5;
|
||||
repeated BodyRange bodyRanges = 6;
|
||||
}
|
||||
|
||||
message Preview {
|
||||
|
|
Ładowanie…
Reference in New Issue