Add text formatting send and receive support for conversations.

main
Cody Henthorne 2023-01-25 10:31:36 -05:00 zatwierdzone przez Greyson Parrelli
rodzic aa2075c78f
commit cc490f4b73
73 zmienionych plików z 1664 dodań i 516 usunięć

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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));

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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,

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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)

Wyświetl plik

@ -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),

Wyświetl plik

@ -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()))
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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)

Wyświetl plik

@ -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()
}

Wyświetl plik

@ -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"
}
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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();
}

Wyświetl plik

@ -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;
}
}
}

Wyświetl plik

@ -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())
}
}
}

Wyświetl plik

@ -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)
}

Wyświetl plik

@ -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()
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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.");
}

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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()

Wyświetl plik

@ -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())
}
}

Wyświetl plik

@ -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
)
)

Wyświetl plik

@ -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
)
)
}

Wyświetl plik

@ -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();

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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
)
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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()) {

Wyświetl plik

@ -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,

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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()

Wyświetl plik

@ -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)

Wyświetl plik

@ -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)

Wyświetl plik

@ -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 {
""
}

Wyświetl plik

@ -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()

Wyświetl plik

@ -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
)
}

Wyświetl plik

@ -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()

Wyświetl plik

@ -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?)
}

Wyświetl plik

@ -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()

Wyświetl plik

@ -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,

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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

Wyświetl plik

@ -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,

Wyświetl plik

@ -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);

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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"))
}
}

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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())
}
}

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -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());

Wyświetl plik

@ -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() ||

Wyświetl plik

@ -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),

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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 {