mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-25 05:27:42 +00:00
Add sync message handling and stop formatting behavior.
This commit is contained in:
committed by
Greyson Parrelli
parent
cedf512726
commit
ee48e6c347
@@ -298,6 +298,8 @@ public class ComposeText extends EmojiEditText {
|
||||
addTextChangedListener(mentionValidatorWatcher);
|
||||
|
||||
if (FeatureFlags.textFormatting()) {
|
||||
addTextChangedListener(new ComposeTextStyleWatcher());
|
||||
|
||||
setCustomSelectionActionModeCallback(new ActionMode.Callback() {
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
@@ -350,7 +352,7 @@ public class ComposeText extends EmojiEditText {
|
||||
}
|
||||
|
||||
if (style != null) {
|
||||
replacement.setSpan(style, 0, charSequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
replacement.setSpan(style, 0, charSequence.length(), MessageStyler.SPAN_FLAGS);
|
||||
}
|
||||
|
||||
clearComposingText();
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package org.thoughtcrime.securesms.components
|
||||
|
||||
import android.text.Annotation
|
||||
import android.text.Editable
|
||||
import android.text.Spannable
|
||||
import android.text.Spanned
|
||||
import android.text.TextUtils
|
||||
import android.text.TextWatcher
|
||||
import android.text.style.CharacterStyle
|
||||
import org.signal.core.util.StringUtil
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler
|
||||
|
||||
/**
|
||||
* Formatting should only grow when appending until a white space character is entered/pasted.
|
||||
*
|
||||
* This watcher observes changes to the text and will shrink supported style ranges as necessary
|
||||
* to provide the desired behavior.
|
||||
*/
|
||||
class ComposeTextStyleWatcher : TextWatcher {
|
||||
private val markerAnnotation = Annotation("text-formatting", "marker")
|
||||
private var textSnapshotPriorToChange: CharSequence? = null
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {
|
||||
if (s is Spannable) {
|
||||
s.removeSpan(markerAnnotation)
|
||||
}
|
||||
|
||||
textSnapshotPriorToChange = s.subSequence(start, start + count)
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
if (s is Spannable) {
|
||||
s.removeSpan(markerAnnotation)
|
||||
|
||||
if (count > 0) {
|
||||
s.setSpan(markerAnnotation, start, start + count, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
val editStart = s.getSpanStart(markerAnnotation)
|
||||
val editEnd = s.getSpanEnd(markerAnnotation)
|
||||
|
||||
s.removeSpan(markerAnnotation)
|
||||
|
||||
if (editStart < 0 || editEnd < 0 || editStart >= editEnd || (editStart == 0 && editEnd == s.length)) {
|
||||
return
|
||||
}
|
||||
|
||||
val change = s.subSequence(editStart, editEnd)
|
||||
if (change.isEmpty() || textSnapshotPriorToChange == null || (editEnd - editStart == 1 && !StringUtil.isVisuallyEmpty(change[0])) || TextUtils.equals(textSnapshotPriorToChange, change)) {
|
||||
textSnapshotPriorToChange = null
|
||||
return
|
||||
}
|
||||
textSnapshotPriorToChange = null
|
||||
|
||||
var newEnd = editStart
|
||||
for (i in change.indices) {
|
||||
if (StringUtil.isVisuallyEmpty(change[i])) {
|
||||
newEnd = editStart + i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
s.getSpans(editStart, editEnd, CharacterStyle::class.java)
|
||||
.filter { MessageStyler.isSupportedCharacterStyle(it) }
|
||||
.forEach { style ->
|
||||
val styleStart = s.getSpanStart(style)
|
||||
val styleEnd = s.getSpanEnd(style)
|
||||
|
||||
if (styleEnd == editEnd && styleStart < styleEnd) {
|
||||
s.removeSpan(style)
|
||||
s.setSpan(style, styleStart, newEnd, MessageStyler.SPAN_FLAGS)
|
||||
} else {
|
||||
s.removeSpan(style)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.util.PlaceholderURLSpan
|
||||
object MessageStyler {
|
||||
|
||||
const val MONOSPACE = "monospace"
|
||||
const val SPAN_FLAGS = Spanned.SPAN_EXCLUSIVE_INCLUSIVE
|
||||
|
||||
@JvmStatic
|
||||
fun boldStyle(): CharacterStyle {
|
||||
@@ -61,7 +62,7 @@ object MessageStyler {
|
||||
}
|
||||
|
||||
if (styleSpan != null) {
|
||||
span.setSpan(styleSpan, range.start, range.start + range.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
span.setSpan(styleSpan, range.start, range.start + range.length, SPAN_FLAGS)
|
||||
appliedStyle = true
|
||||
}
|
||||
} else if (range.hasLink() && range.link != null) {
|
||||
@@ -121,6 +122,7 @@ object MessageStyler {
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isSupportedCharacterStyle(style: CharacterStyle): Boolean {
|
||||
return when (style) {
|
||||
is StyleSpan -> style.style == Typeface.ITALIC || style.style == Typeface.BOLD
|
||||
|
||||
@@ -1347,7 +1347,7 @@ public class MessageContentProcessor {
|
||||
threadId = SignalDatabase.threads().getOrCreateThreadIdFor(getSyncMessageDestination(message));
|
||||
} else if (dataMessage.getRemoteDelete().isPresent()) {
|
||||
handleRemoteDelete(content, dataMessage, senderRecipient, processingEarlyContent);
|
||||
} else if (dataMessage.getAttachments().isPresent() || dataMessage.getQuote().isPresent() || dataMessage.getPreviews().isPresent() || dataMessage.getSticker().isPresent() || dataMessage.isViewOnce() || dataMessage.getMentions().isPresent()) {
|
||||
} else if (dataMessage.getAttachments().isPresent() || dataMessage.getQuote().isPresent() || dataMessage.getPreviews().isPresent() || dataMessage.getSticker().isPresent() || dataMessage.isViewOnce() || dataMessage.getMentions().isPresent() || dataMessage.getBodyRanges().isPresent()) {
|
||||
threadId = handleSynchronizeSentMediaMessage(message, content.getTimestamp());
|
||||
} else {
|
||||
threadId = handleSynchronizeSentTextMessage(message, content.getTimestamp());
|
||||
@@ -2094,12 +2094,14 @@ public class MessageContentProcessor {
|
||||
MmsMessageRecord story = (MmsMessageRecord) database.getMessageRecord(storyMessageId.getId());
|
||||
Recipient threadRecipient = SignalDatabase.threads().getRecipientForThreadId(story.getThreadId());
|
||||
boolean groupStory = threadRecipient != null && threadRecipient.isActiveGroup();
|
||||
BodyRangeList bodyRanges = null;
|
||||
String body;
|
||||
|
||||
if (reaction.isPresent() && EmojiUtil.isEmoji(reaction.get().getEmoji())) {
|
||||
body = reaction.get().getEmoji();
|
||||
} else {
|
||||
body = message.getDataMessage().get().getBody().orElse(null);
|
||||
body = message.getDataMessage().get().getBody().orElse(null);
|
||||
bodyRanges = getBodyRangeList(message.getDataMessage().get().getBodyRanges());
|
||||
}
|
||||
|
||||
if (message.getDataMessage().get().getGroupContext().isPresent()) {
|
||||
@@ -2107,15 +2109,15 @@ public class MessageContentProcessor {
|
||||
} else if (groupStory || story.getStoryType().isStoryWithReplies()) {
|
||||
parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId());
|
||||
|
||||
String quoteBody = "";
|
||||
BodyRangeList bodyRanges = null;
|
||||
String quoteBody = "";
|
||||
BodyRangeList quotedBodyRanges = null;
|
||||
|
||||
if (story.getStoryType().isTextStory()) {
|
||||
quoteBody = story.getBody();
|
||||
bodyRanges = story.getMessageRanges();
|
||||
quoteBody = story.getBody();
|
||||
quotedBodyRanges = story.getMessageRanges();
|
||||
}
|
||||
|
||||
quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, quoteBody, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL, bodyRanges);
|
||||
quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, quoteBody, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL, quotedBodyRanges);
|
||||
expiresInMillis = TimeUnit.SECONDS.toMillis(message.getDataMessage().get().getExpiresInSeconds());
|
||||
} else {
|
||||
warn(envelopeTimestamp, "Story has replies disabled. Dropping reply.");
|
||||
@@ -2141,7 +2143,7 @@ public class MessageContentProcessor {
|
||||
Collections.emptySet(),
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
bodyRanges,
|
||||
-1);
|
||||
|
||||
if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
|
||||
@@ -2205,6 +2207,7 @@ public class MessageContentProcessor {
|
||||
Set<DistributionId> distributionIds = manifest.getDistributionIdSet();
|
||||
Optional<GroupId> groupId = storyMessage.getGroupContext().map(it -> GroupId.v2(it.getMasterKey()));
|
||||
String textStoryBody = storyMessage.getTextAttachment().map(this::serializeTextAttachment).orElse(null);
|
||||
BodyRangeList bodyRanges = getBodyRangeList(storyMessage.getBodyRanges());
|
||||
StoryType storyType = getStoryType(storyMessage);
|
||||
List<LinkPreview> linkPreviews = getLinkPreviews(storyMessage.getTextAttachment().flatMap(t -> t.getPreview().map(Collections::singletonList)),
|
||||
"",
|
||||
@@ -2216,13 +2219,13 @@ public class MessageContentProcessor {
|
||||
for (final DistributionId distributionId : distributionIds) {
|
||||
RecipientId distributionRecipientId = SignalDatabase.distributionLists().getOrCreateByDistributionId(distributionId, manifest);
|
||||
Recipient distributionListRecipient = Recipient.resolved(distributionRecipientId);
|
||||
insertSentStoryMessage(message, distributionListRecipient, textStoryBody, attachments, message.getTimestamp(), storyType, linkPreviews);
|
||||
insertSentStoryMessage(message, distributionListRecipient, textStoryBody, attachments, message.getTimestamp(), storyType, linkPreviews, bodyRanges);
|
||||
}
|
||||
|
||||
if (groupId.isPresent()) {
|
||||
Optional<RecipientId> groupRecipient = SignalDatabase.recipients().getByGroupId(groupId.get());
|
||||
if (groupRecipient.isPresent()) {
|
||||
insertSentStoryMessage(message, Recipient.resolved(groupRecipient.get()), textStoryBody, attachments, message.getTimestamp(), storyType, linkPreviews);
|
||||
insertSentStoryMessage(message, Recipient.resolved(groupRecipient.get()), textStoryBody, attachments, message.getTimestamp(), storyType, linkPreviews, bodyRanges);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2235,7 +2238,8 @@ public class MessageContentProcessor {
|
||||
@NonNull List<Attachment> pendingAttachments,
|
||||
long sentAtTimestamp,
|
||||
@NonNull StoryType storyType,
|
||||
@NonNull List<LinkPreview> linkPreviews)
|
||||
@NonNull List<LinkPreview> linkPreviews,
|
||||
@Nullable BodyRangeList bodyRanges)
|
||||
throws MmsException
|
||||
{
|
||||
if (SignalDatabase.messages().isOutgoingStoryAlreadyInDatabase(recipient.getId(), sentAtTimestamp)) {
|
||||
@@ -2262,7 +2266,7 @@ public class MessageContentProcessor {
|
||||
Collections.emptySet(),
|
||||
null,
|
||||
true,
|
||||
null,
|
||||
bodyRanges,
|
||||
-1);
|
||||
|
||||
MessageTable messageTable = SignalDatabase.messages();
|
||||
@@ -2335,6 +2339,7 @@ public class MessageContentProcessor {
|
||||
Optional<List<Mention>> mentions = getMentions(message.getDataMessage().get().getMentions());
|
||||
Optional<GiftBadge> giftBadge = getGiftBadge(message.getDataMessage().get().getGiftBadge());
|
||||
boolean viewOnce = message.getDataMessage().get().isViewOnce();
|
||||
BodyRangeList bodyRanges = getBodyRangeList(message.getDataMessage().get().getBodyRanges());
|
||||
List<Attachment> syncAttachments = viewOnce ? Collections.singletonList(new TombstoneAttachment(MediaUtil.VIEW_ONCE, false))
|
||||
: PointerAttachment.forPointers(message.getDataMessage().get().getAttachments());
|
||||
|
||||
@@ -2361,7 +2366,7 @@ public class MessageContentProcessor {
|
||||
Collections.emptySet(),
|
||||
giftBadge.orElse(null),
|
||||
true,
|
||||
null,
|
||||
bodyRanges,
|
||||
-1);
|
||||
|
||||
if (recipients.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
|
||||
@@ -3182,7 +3187,7 @@ public class MessageContentProcessor {
|
||||
}
|
||||
|
||||
private @Nullable BodyRangeList getBodyRangeList(Optional<List<SignalServiceProtos.BodyRange>> bodyRanges) {
|
||||
if (!bodyRanges.isPresent()) {
|
||||
if (bodyRanges.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user