Add text formatting send and receive support for conversations.

This commit is contained in:
Cody Henthorne
2023-01-25 10:31:36 -05:00
committed by Greyson Parrelli
parent aa2075c78f
commit cc490f4b73
73 changed files with 1664 additions and 516 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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