mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 00:59:49 +01:00
Add text formatting send and receive support for conversations.
This commit is contained in:
committed by
Greyson Parrelli
parent
aa2075c78f
commit
cc490f4b73
@@ -166,7 +166,7 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
||||
stopwatch.split("recipient-resolves");
|
||||
|
||||
List<ConversationMessage> messages = Stream.of(records)
|
||||
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, mentionHelper.getMentions(m.getId()), quotedHelper.isQuoted(m.getId())))
|
||||
.map(m -> ConversationMessageFactory.createWithUnresolvedData(context, m, m.getDisplayBody(context), mentionHelper.getMentions(m.getId()), quotedHelper.isQuoted(m.getId())))
|
||||
.toList();
|
||||
|
||||
stopwatch.split("conversion");
|
||||
@@ -220,7 +220,11 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
||||
|
||||
stopwatch.split("calls");
|
||||
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(), record, mentions, isQuoted);
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(),
|
||||
record,
|
||||
record.getDisplayBody(ApplicationDependencies.getApplication()),
|
||||
mentions,
|
||||
isQuoted);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -181,6 +181,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
private static final long MAX_CLUSTERING_TIME_DIFF = TimeUnit.MINUTES.toMillis(3);
|
||||
private static final int CONDENSED_MODE_MAX_LINES = 3;
|
||||
|
||||
private static final SearchUtil.StyleFactory STYLE_FACTORY = () -> new CharacterStyle[] { new BackgroundColorSpan(Color.YELLOW), new ForegroundColorSpan(Color.BLACK) };
|
||||
|
||||
private ConversationMessage conversationMessage;
|
||||
private MessageRecord messageRecord;
|
||||
private Optional<MessageRecord> nextMessageRecord;
|
||||
@@ -988,8 +990,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
if (messageRequestAccepted) {
|
||||
linkifyMessageBody(styledText, batchSelected.isEmpty());
|
||||
}
|
||||
styledText = SearchUtil.getHighlightedSpan(locale, () -> new BackgroundColorSpan(Color.YELLOW), styledText, searchQuery, SearchUtil.STRICT);
|
||||
styledText = SearchUtil.getHighlightedSpan(locale, () -> new ForegroundColorSpan(Color.BLACK), styledText, searchQuery, SearchUtil.STRICT);
|
||||
styledText = SearchUtil.getHighlightedSpan(locale, STYLE_FACTORY, styledText, searchQuery, SearchUtil.STRICT);
|
||||
|
||||
if (hasExtraText(messageRecord)) {
|
||||
bodyText.setOverflowText(getLongMessageSpan(messageRecord));
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.conversation;
|
||||
import android.content.Context;
|
||||
import android.text.SpannableString;
|
||||
|
||||
import androidx.annotation.AnyThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
@@ -12,6 +11,7 @@ import org.signal.core.util.Conversions;
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.Multiselect;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectCollection;
|
||||
import org.thoughtcrime.securesms.database.BodyRangeUtil;
|
||||
import org.thoughtcrime.securesms.database.MentionUtil;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
@@ -35,22 +35,20 @@ public class ConversationMessage {
|
||||
@NonNull private final MessageStyler.Result styleResult;
|
||||
private final boolean hasBeenQuoted;
|
||||
|
||||
private ConversationMessage(@NonNull MessageRecord messageRecord, boolean hasBeenQuoted) {
|
||||
this(messageRecord, null, null, hasBeenQuoted);
|
||||
}
|
||||
|
||||
private ConversationMessage(@NonNull MessageRecord messageRecord,
|
||||
@Nullable CharSequence body,
|
||||
@Nullable List<Mention> mentions,
|
||||
boolean hasBeenQuoted)
|
||||
boolean hasBeenQuoted,
|
||||
@Nullable MessageStyler.Result styleResult)
|
||||
{
|
||||
this.messageRecord = messageRecord;
|
||||
this.hasBeenQuoted = hasBeenQuoted;
|
||||
this.mentions = mentions != null ? mentions : Collections.emptyList();
|
||||
this.styleResult = styleResult != null ? styleResult : MessageStyler.Result.none();
|
||||
|
||||
if (body != null) {
|
||||
this.body = SpannableString.valueOf(body);
|
||||
} else if (messageRecord.hasMessageRanges()) {
|
||||
} else if (messageRecord.getMessageRanges() != null) {
|
||||
this.body = SpannableString.valueOf(messageRecord.getBody());
|
||||
} else {
|
||||
this.body = null;
|
||||
@@ -60,12 +58,6 @@ public class ConversationMessage {
|
||||
MentionAnnotation.setMentionAnnotations(this.body, this.mentions);
|
||||
}
|
||||
|
||||
if (this.body != null && messageRecord.hasMessageRanges()) {
|
||||
styleResult = MessageStyler.style(messageRecord.requireMessageRanges(), this.body);
|
||||
} else {
|
||||
styleResult = MessageStyler.Result.none();
|
||||
}
|
||||
|
||||
multiselectCollection = Multiselect.getParts(this);
|
||||
}
|
||||
|
||||
@@ -128,32 +120,6 @@ public class ConversationMessage {
|
||||
*/
|
||||
public static class ConversationMessageFactory {
|
||||
|
||||
/**
|
||||
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord. No database or
|
||||
* heavy work performed as the message is assumed to not have any mentions.
|
||||
*/
|
||||
@AnyThread
|
||||
public static @NonNull ConversationMessage createWithResolvedData(@NonNull MessageRecord messageRecord, boolean hasBeenQuoted) {
|
||||
return new ConversationMessage(messageRecord, hasBeenQuoted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord, potentially annotated body, and
|
||||
* list of actual mentions. No database or heavy work performed as the body and mentions are assumed to be
|
||||
* fully updated with display names.
|
||||
*
|
||||
* @param body Contains appropriate {@link MentionAnnotation}s and is updated with actual profile names.
|
||||
* @param mentions List of actual mentions (i.e., not placeholder) matching annotation ranges in body.
|
||||
* @param hasBeenQuoted Whether or not the message has been quoted by another message.
|
||||
*/
|
||||
@AnyThread
|
||||
public static @NonNull ConversationMessage createWithResolvedData(@NonNull MessageRecord messageRecord, @Nullable CharSequence body, @Nullable List<Mention> mentions, boolean hasBeenQuoted) {
|
||||
if (messageRecord.isMms() && mentions != null && !mentions.isEmpty()) {
|
||||
return new ConversationMessage(messageRecord, body, mentions, hasBeenQuoted);
|
||||
}
|
||||
return new ConversationMessage(messageRecord, body, null, hasBeenQuoted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link ConversationMessage} wrapping the provided MessageRecord and will update and modify the provided
|
||||
* mentions from placeholder to actual. This method may perform database operations to resolve mentions to display names.
|
||||
@@ -161,12 +127,33 @@ public class ConversationMessage {
|
||||
* @param mentions List of placeholder mentions to be used to update the body in the provided MessageRecord.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, @Nullable List<Mention> mentions, boolean hasBeenQuoted) {
|
||||
if (messageRecord.isMms() && mentions != null && !mentions.isEmpty()) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, messageRecord, mentions);
|
||||
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions(), hasBeenQuoted);
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context,
|
||||
@NonNull MessageRecord messageRecord,
|
||||
@NonNull CharSequence body,
|
||||
@Nullable List<Mention> mentions,
|
||||
boolean hasBeenQuoted)
|
||||
{
|
||||
SpannableString styledAndMentionBody = null;
|
||||
MessageStyler.Result styleResult = MessageStyler.Result.none();
|
||||
|
||||
MentionUtil.UpdatedBodyAndMentions mentionsUpdate = null;
|
||||
if (mentions != null && !mentions.isEmpty()) {
|
||||
mentionsUpdate = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, body, mentions);
|
||||
}
|
||||
return createWithResolvedData(messageRecord, hasBeenQuoted);
|
||||
|
||||
if (messageRecord.getMessageRanges() != null) {
|
||||
BodyRangeList bodyRanges = mentionsUpdate == null ? messageRecord.getMessageRanges()
|
||||
: BodyRangeUtil.adjustBodyRanges(messageRecord.getMessageRanges(), mentionsUpdate.getBodyAdjustments());
|
||||
|
||||
styledAndMentionBody = SpannableString.valueOf(mentionsUpdate != null ? mentionsUpdate.getBody() : body);
|
||||
styleResult = MessageStyler.style(bodyRanges, styledAndMentionBody);
|
||||
}
|
||||
|
||||
return new ConversationMessage(messageRecord,
|
||||
styledAndMentionBody != null ? styledAndMentionBody : mentionsUpdate != null ? mentionsUpdate.getBody() : body,
|
||||
mentionsUpdate != null ? mentionsUpdate.getMentions() : null,
|
||||
hasBeenQuoted,
|
||||
styleResult);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -185,17 +172,10 @@ public class ConversationMessage {
|
||||
* database operations to query for mentions and then to resolve mentions to display names.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body) {
|
||||
boolean hasBeenQuoted = SignalDatabase.messages().isQuoted(messageRecord);
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId());
|
||||
if (!mentions.isEmpty()) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, body, mentions);
|
||||
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions(), hasBeenQuoted);
|
||||
}
|
||||
}
|
||||
return createWithResolvedData(messageRecord, body, null, hasBeenQuoted);
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, boolean hasBeenQuoted) {
|
||||
List<Mention> mentions = messageRecord.isMms() ? SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId())
|
||||
: null;
|
||||
return createWithUnresolvedData(context, messageRecord, messageRecord.getDisplayBody(context), mentions, hasBeenQuoted);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,15 +184,11 @@ public class ConversationMessage {
|
||||
* database operations to query for mentions and then to resolve mentions to display names.
|
||||
*/
|
||||
@WorkerThread
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body, boolean hasBeenQuoted) {
|
||||
if (messageRecord.isMms()) {
|
||||
List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId());
|
||||
if (!mentions.isEmpty()) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, body, mentions);
|
||||
return new ConversationMessage(messageRecord, updated.getBody(), updated.getMentions(), hasBeenQuoted);
|
||||
}
|
||||
}
|
||||
return createWithResolvedData(messageRecord, body, null, hasBeenQuoted);
|
||||
public static @NonNull ConversationMessage createWithUnresolvedData(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body) {
|
||||
boolean hasBeenQuoted = SignalDatabase.messages().isQuoted(messageRecord);
|
||||
List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId());
|
||||
|
||||
return createWithUnresolvedData(context, messageRecord, body, mentions, hasBeenQuoted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +186,7 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.database.model.StoryType;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.GroupCallPeekEvent;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
@@ -748,11 +749,12 @@ public class ConversationParentFragment extends Fragment
|
||||
return;
|
||||
}
|
||||
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
boolean initiating = threadId == -1;
|
||||
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orElse(null);
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
List<Mention> mentions = new ArrayList<>(result.getMentions());
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
boolean initiating = threadId == -1;
|
||||
QuoteModel quote = result.isViewOnce() ? null : inputPanel.getQuote().orElse(null);
|
||||
SlideDeck slideDeck = new SlideDeck();
|
||||
List<Mention> mentions = new ArrayList<>(result.getMentions());
|
||||
BodyRangeList bodyRanges = result.getBodyRanges();
|
||||
|
||||
for (Media mediaItem : result.getNonUploadedMedia()) {
|
||||
if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
|
||||
@@ -776,6 +778,7 @@ public class ConversationParentFragment extends Fragment
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
mentions,
|
||||
bodyRanges,
|
||||
expiresIn,
|
||||
result.isViewOnce(),
|
||||
initiating,
|
||||
@@ -1840,7 +1843,7 @@ public class ConversationParentFragment extends Fragment
|
||||
quoteResult.addListener(listener);
|
||||
break;
|
||||
case Draft.VOICE_NOTE:
|
||||
case Draft.MENTION:
|
||||
case Draft.BODY_RANGES:
|
||||
listener.onSuccess(true);
|
||||
break;
|
||||
}
|
||||
@@ -2704,7 +2707,7 @@ public class ConversationParentFragment extends Fragment
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.get().getExpiresInSeconds());
|
||||
boolean initiating = threadId == -1;
|
||||
|
||||
sendMediaMessage(recipient.getId(), sendButton.getSelectedSendType(), "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), Collections.emptyList(), expiresIn, false, initiating, false, null);
|
||||
sendMediaMessage(recipient.getId(), sendButton.getSelectedSendType(), "", attachmentManager.buildSlideDeck(), null, contacts, Collections.emptyList(), Collections.emptyList(), null, expiresIn, false, initiating, false, null);
|
||||
}
|
||||
|
||||
private void selectContactInfo(ContactData contactData) {
|
||||
@@ -2943,6 +2946,7 @@ public class ConversationParentFragment extends Fragment
|
||||
recipient.getEmail().isPresent() ||
|
||||
inputPanel.getQuote().isPresent() ||
|
||||
composeText.hasMentions() ||
|
||||
composeText.hasStyling() ||
|
||||
linkPreviewViewModel.hasLinkPreview() ||
|
||||
needsSplit;
|
||||
|
||||
@@ -2997,9 +3001,10 @@ public class ConversationParentFragment extends Fragment
|
||||
Collections.emptySet(),
|
||||
Collections.emptySet(),
|
||||
null,
|
||||
true);
|
||||
true,
|
||||
result.getBodyRanges());
|
||||
|
||||
final Context context = requireContext().getApplicationContext();
|
||||
final Context context = requireContext().getApplicationContext();
|
||||
|
||||
ApplicationDependencies.getTypingStatusSender().onTypingStopped(thread);
|
||||
|
||||
@@ -3032,6 +3037,7 @@ public class ConversationParentFragment extends Fragment
|
||||
Collections.emptyList(),
|
||||
linkPreviews,
|
||||
composeText.getMentions(),
|
||||
composeText.getStyling(),
|
||||
expiresIn,
|
||||
viewOnce,
|
||||
initiating,
|
||||
@@ -3047,6 +3053,7 @@ public class ConversationParentFragment extends Fragment
|
||||
List<Contact> contacts,
|
||||
List<LinkPreview> previews,
|
||||
List<Mention> mentions,
|
||||
@Nullable BodyRangeList styling,
|
||||
final long expiresIn,
|
||||
final boolean viewOnce,
|
||||
final boolean initiating,
|
||||
@@ -3093,7 +3100,8 @@ public class ConversationParentFragment extends Fragment
|
||||
Collections.emptySet(),
|
||||
Collections.emptySet(),
|
||||
null,
|
||||
false);
|
||||
false,
|
||||
styling);
|
||||
|
||||
final SettableFuture<Void> future = new SettableFuture<>();
|
||||
final Context context = requireContext().getApplicationContext();
|
||||
@@ -3154,7 +3162,7 @@ public class ConversationParentFragment extends Fragment
|
||||
OutgoingMessage message;
|
||||
|
||||
if (sendPush) {
|
||||
message = OutgoingMessage.text(recipient.get(), messageBody, expiresIn, System.currentTimeMillis());
|
||||
message = OutgoingMessage.text(recipient.get(), messageBody, expiresIn, System.currentTimeMillis(), null);
|
||||
ApplicationDependencies.getTypingStatusSender().onTypingStopped(thread);
|
||||
} else {
|
||||
message = OutgoingMessage.sms(recipient.get(), messageBody, sendType.getSimSubscriptionIdOr(-1));
|
||||
@@ -3411,6 +3419,7 @@ public class ConversationParentFragment extends Fragment
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
composeText.getMentions(),
|
||||
composeText.getStyling(),
|
||||
expiresIn,
|
||||
false,
|
||||
initiating,
|
||||
@@ -3443,7 +3452,7 @@ public class ConversationParentFragment extends Fragment
|
||||
|
||||
slideDeck.addSlide(stickerSlide);
|
||||
|
||||
sendMediaMessage(recipient.getId(), sendType, "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), expiresIn, false, initiating, clearCompose, null);
|
||||
sendMediaMessage(recipient.getId(), sendType, "", slideDeck, null, Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), null, expiresIn, false, initiating, clearCompose, null);
|
||||
}
|
||||
|
||||
private void silentlySetComposeText(String text) {
|
||||
@@ -3746,7 +3755,7 @@ public class ConversationParentFragment extends Fragment
|
||||
}
|
||||
|
||||
private void handleSaveDraftOnTextChange(@NonNull CharSequence text) {
|
||||
textDraftSaveDebouncer.publish(() -> draftViewModel.setTextDraft(StringUtil.trimSequence(text).toString(), MentionAnnotation.getMentionsFromAnnotations(text)));
|
||||
textDraftSaveDebouncer.publish(() -> draftViewModel.setTextDraft(StringUtil.trimSequence(text).toString(), MentionAnnotation.getMentionsFromAnnotations(text), MessageStyler.getStyling(text)));
|
||||
}
|
||||
|
||||
private void handleTypingIndicatorOnTextChange(@NonNull String text) {
|
||||
@@ -4185,6 +4194,7 @@ public class ConversationParentFragment extends Fragment
|
||||
Collections.emptyList(),
|
||||
Collections.emptyList(),
|
||||
composeText.getMentions(),
|
||||
composeText.getStyling(),
|
||||
expiresIn,
|
||||
false,
|
||||
initiating,
|
||||
|
||||
@@ -1,45 +1,133 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.SpannableString
|
||||
import android.text.Spannable
|
||||
import android.text.Spanned
|
||||
import android.text.style.CharacterStyle
|
||||
import android.text.style.StrikethroughSpan
|
||||
import android.text.style.StyleSpan
|
||||
import android.text.style.TypefaceSpan
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.PlaceholderURLSpan
|
||||
|
||||
/**
|
||||
* Helper for applying style-based [BodyRangeList.BodyRange]s to text.
|
||||
* Helper for parsing and applying styles. Most notably with [BodyRangeList].
|
||||
*/
|
||||
object MessageStyler {
|
||||
|
||||
const val MONOSPACE = "monospace"
|
||||
|
||||
@JvmStatic
|
||||
fun style(messageRanges: BodyRangeList, span: SpannableString): Result {
|
||||
fun boldStyle(): CharacterStyle {
|
||||
return StyleSpan(Typeface.BOLD)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun italicStyle(): CharacterStyle {
|
||||
return StyleSpan(Typeface.ITALIC)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun strikethroughStyle(): CharacterStyle {
|
||||
return StrikethroughSpan()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun monoStyle(): CharacterStyle {
|
||||
return TypefaceSpan(MONOSPACE)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun style(messageRanges: BodyRangeList?, span: Spannable): Result {
|
||||
if (messageRanges == null) {
|
||||
return Result.none()
|
||||
}
|
||||
|
||||
var appliedStyle = false
|
||||
var hasLinks = false
|
||||
var bottomButton: BodyRangeList.BodyRange.Button? = null
|
||||
|
||||
for (range in messageRanges.rangesList) {
|
||||
if (range.hasStyle()) {
|
||||
messageRanges
|
||||
.rangesList
|
||||
.filter { r -> r.start >= 0 && r.start < span.length && r.start + r.length >= 0 && r.start + r.length <= span.length }
|
||||
.forEach { range ->
|
||||
if (range.hasStyle()) {
|
||||
val styleSpan: CharacterStyle? = when (range.style) {
|
||||
BodyRangeList.BodyRange.Style.BOLD -> boldStyle()
|
||||
BodyRangeList.BodyRange.Style.ITALIC -> italicStyle()
|
||||
BodyRangeList.BodyRange.Style.STRIKETHROUGH -> strikethroughStyle()
|
||||
BodyRangeList.BodyRange.Style.MONOSPACE -> monoStyle()
|
||||
else -> null
|
||||
}
|
||||
|
||||
val styleSpan: CharacterStyle? = when (range.style) {
|
||||
BodyRangeList.BodyRange.Style.BOLD -> TypefaceSpan("sans-serif-medium")
|
||||
BodyRangeList.BodyRange.Style.ITALIC -> StyleSpan(Typeface.ITALIC)
|
||||
else -> null
|
||||
if (styleSpan != null) {
|
||||
span.setSpan(styleSpan, range.start, range.start + range.length, Spanned.SPAN_EXCLUSIVE_INCLUSIVE)
|
||||
appliedStyle = true
|
||||
}
|
||||
} else if (range.hasLink() && range.link != null) {
|
||||
span.setSpan(PlaceholderURLSpan(range.link), range.start, range.start + range.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
hasLinks = true
|
||||
} else if (range.hasButton() && range.button != null) {
|
||||
bottomButton = range.button
|
||||
}
|
||||
|
||||
if (styleSpan != null) {
|
||||
span.setSpan(styleSpan, range.start, range.start + range.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
} else if (range.hasLink() && range.link != null) {
|
||||
span.setSpan(PlaceholderURLSpan(range.link), range.start, range.start + range.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
hasLinks = true
|
||||
} else if (range.hasButton() && range.button != null) {
|
||||
bottomButton = range.button
|
||||
}
|
||||
|
||||
return if (appliedStyle || hasLinks || bottomButton != null) {
|
||||
Result(hasLinks, bottomButton)
|
||||
} else {
|
||||
Result.none()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun hasStyling(text: Spanned): Boolean {
|
||||
return if (FeatureFlags.textFormatting()) {
|
||||
text.getSpans(0, text.length, CharacterStyle::class.java)
|
||||
.any { s -> isSupportedCharacterStyle(s) }
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getStyling(text: CharSequence?): BodyRangeList? {
|
||||
val bodyRanges = if (text is Spanned && FeatureFlags.textFormatting()) {
|
||||
text
|
||||
.getSpans(0, text.length, CharacterStyle::class.java)
|
||||
.filter { s -> isSupportedCharacterStyle(s) }
|
||||
.map { span: CharacterStyle ->
|
||||
val spanStart = text.getSpanStart(span)
|
||||
val spanLength = text.getSpanEnd(span) - spanStart
|
||||
|
||||
val style = when (span) {
|
||||
is StyleSpan -> if (span.style == Typeface.BOLD) BodyRangeList.BodyRange.Style.BOLD else BodyRangeList.BodyRange.Style.ITALIC
|
||||
is StrikethroughSpan -> BodyRangeList.BodyRange.Style.STRIKETHROUGH
|
||||
is TypefaceSpan -> BodyRangeList.BodyRange.Style.MONOSPACE
|
||||
else -> throw IllegalArgumentException("Provided text contains unsupported spans")
|
||||
}
|
||||
|
||||
BodyRangeList.BodyRange.newBuilder().setStart(spanStart).setLength(spanLength).setStyle(style).build()
|
||||
}
|
||||
.toList()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
return Result(hasLinks, bottomButton)
|
||||
return if (bodyRanges.isNotEmpty()) {
|
||||
BodyRangeList.newBuilder().addAllRanges(bodyRanges).build()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun isSupportedCharacterStyle(style: CharacterStyle): Boolean {
|
||||
return when (style) {
|
||||
is StyleSpan -> style.style == Typeface.ITALIC || style.style == Typeface.BOLD
|
||||
is StrikethroughSpan -> true
|
||||
is TypefaceSpan -> style.family == MONOSPACE
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
data class Result(val hasStyleLinks: Boolean = false, val bottomButton: BodyRangeList.BodyRange.Button? = null) {
|
||||
|
||||
@@ -8,12 +8,16 @@ import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler
|
||||
import org.thoughtcrime.securesms.database.DraftTable
|
||||
import org.thoughtcrime.securesms.database.DraftTable.Drafts
|
||||
import org.thoughtcrime.securesms.database.MentionUtil
|
||||
import org.thoughtcrime.securesms.database.MessageTypes
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.database.adjustBodyRanges
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -57,15 +61,18 @@ class DraftRepository(
|
||||
fun loadDrafts(threadId: Long): Single<DatabaseDraft> {
|
||||
return Single.fromCallable {
|
||||
val drafts: Drafts = draftTable.getDrafts(threadId)
|
||||
val mentionsDraft = drafts.getDraftOfType(DraftTable.Draft.MENTION)
|
||||
val bodyRangesDraft = drafts.getDraftOfType(DraftTable.Draft.BODY_RANGES)
|
||||
var updatedText: Spannable? = null
|
||||
|
||||
if (mentionsDraft != null) {
|
||||
val text = drafts.getDraftOfType(DraftTable.Draft.TEXT)!!.value
|
||||
val mentions = MentionUtil.bodyRangeListToMentions(context, Base64.decodeOrThrow(mentionsDraft.value))
|
||||
val updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, text, mentions)
|
||||
if (bodyRangesDraft != null) {
|
||||
val bodyRanges: BodyRangeList = BodyRangeList.parseFrom(Base64.decodeOrThrow(bodyRangesDraft.value))
|
||||
val mentions: List<Mention> = MentionUtil.bodyRangeListToMentions(bodyRanges)
|
||||
|
||||
val updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, drafts.getDraftOfType(DraftTable.Draft.TEXT)!!.value, mentions)
|
||||
|
||||
updatedText = SpannableString(updated.body)
|
||||
MentionAnnotation.setMentionAnnotations(updatedText, updated.mentions)
|
||||
MessageStyler.style(bodyRanges.adjustBodyRanges(updated.bodyAdjustments), updatedText)
|
||||
}
|
||||
|
||||
DatabaseDraft(drafts, updatedText)
|
||||
|
||||
@@ -14,7 +14,7 @@ data class DraftState(
|
||||
val threadId: Long = -1,
|
||||
val distributionType: Int = 0,
|
||||
val textDraft: DraftTable.Draft? = null,
|
||||
val mentionsDraft: DraftTable.Draft? = null,
|
||||
val bodyRangesDraft: DraftTable.Draft? = null,
|
||||
val quoteDraft: DraftTable.Draft? = null,
|
||||
val locationDraft: DraftTable.Draft? = null,
|
||||
val voiceNoteDraft: DraftTable.Draft? = null,
|
||||
@@ -27,7 +27,7 @@ data class DraftState(
|
||||
fun toDrafts(): Drafts {
|
||||
return Drafts().apply {
|
||||
addIfNotNull(textDraft)
|
||||
addIfNotNull(mentionsDraft)
|
||||
addIfNotNull(bodyRangesDraft)
|
||||
addIfNotNull(quoteDraft)
|
||||
addIfNotNull(locationDraft)
|
||||
addIfNotNull(voiceNoteDraft)
|
||||
@@ -38,7 +38,7 @@ data class DraftState(
|
||||
return copy(
|
||||
threadId = threadId,
|
||||
textDraft = drafts.getDraftOfType(DraftTable.Draft.TEXT),
|
||||
mentionsDraft = drafts.getDraftOfType(DraftTable.Draft.MENTION),
|
||||
bodyRangesDraft = drafts.getDraftOfType(DraftTable.Draft.BODY_RANGES),
|
||||
quoteDraft = drafts.getDraftOfType(DraftTable.Draft.QUOTE),
|
||||
locationDraft = drafts.getDraftOfType(DraftTable.Draft.LOCATION),
|
||||
voiceNoteDraft = drafts.getDraftOfType(DraftTable.Draft.VOICE_NOTE),
|
||||
|
||||
@@ -64,9 +64,19 @@ class DraftViewModel @JvmOverloads constructor(
|
||||
store.update { it.copy(recipientId = recipient.id) }
|
||||
}
|
||||
|
||||
fun setTextDraft(text: String, mentions: List<Mention>) {
|
||||
fun setTextDraft(text: String, mentions: List<Mention>, styleBodyRanges: BodyRangeList?) {
|
||||
store.update {
|
||||
saveDrafts(it.copy(textDraft = text.toTextDraft(), mentionsDraft = mentions.toMentionsDraft()))
|
||||
val mentionRanges: BodyRangeList? = MentionUtil.mentionsToBodyRangeList(mentions)
|
||||
|
||||
val bodyRanges: BodyRangeList? = if (styleBodyRanges == null) {
|
||||
mentionRanges
|
||||
} else if (mentionRanges == null) {
|
||||
styleBodyRanges
|
||||
} else {
|
||||
styleBodyRanges.toBuilder().addAllRanges(mentionRanges.rangesList).build()
|
||||
}
|
||||
|
||||
saveDrafts(it.copy(textDraft = text.toTextDraft(), bodyRangesDraft = bodyRanges?.toDraft()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,11 +128,6 @@ private fun String.toTextDraft(): Draft? {
|
||||
return if (isNotEmpty()) Draft(Draft.TEXT, this) else null
|
||||
}
|
||||
|
||||
private fun List<Mention>.toMentionsDraft(): Draft? {
|
||||
val mentions: BodyRangeList? = MentionUtil.mentionsToBodyRangeList(this)
|
||||
return if (mentions != null) {
|
||||
Draft(Draft.MENTION, Base64.encodeBytes(mentions.toByteArray()))
|
||||
} else {
|
||||
null
|
||||
}
|
||||
private fun BodyRangeList.toDraft(): Draft {
|
||||
return Draft(Draft.BODY_RANGES, Base64.encodeBytes(toByteArray()))
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ data class MultiselectForwardFragmentArgs @JvmOverloads constructor(
|
||||
.withMentions(conversationMessage.mentions)
|
||||
.withTimestamp(conversationMessage.messageRecord.timestamp)
|
||||
.withExpiration(conversationMessage.messageRecord.expireStarted + conversationMessage.messageRecord.expiresIn)
|
||||
.withBodyRanges(conversationMessage.messageRecord.messageRanges)
|
||||
|
||||
if (conversationMessage.multiselectCollection.isTextSelected(selectedParts)) {
|
||||
val mediaMessage: MmsMessageRecord? = conversationMessage.messageRecord as? MmsMessageRecord
|
||||
|
||||
@@ -99,7 +99,7 @@ class MessageQuotesRepository {
|
||||
.buildUpdatedModels(ApplicationDependencies.getApplication(), listOf(originalRecord))
|
||||
.get(0)
|
||||
|
||||
val originalMessage: ConversationMessage = ConversationMessageFactory.createWithUnresolvedData(application, originalRecord, originalRecord.getDisplayBody(application), false)
|
||||
val originalMessage: ConversationMessage = ConversationMessageFactory.createWithUnresolvedData(application, originalRecord, false)
|
||||
|
||||
return replies + originalMessage
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user