mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01:00
Add text formatting send and receive support for conversations.
This commit is contained in:
committed by
Greyson Parrelli
parent
aa2075c78f
commit
cc490f4b73
@@ -0,0 +1,6 @@
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
/**
|
||||
* Store data about an operation that changes the contents of a body.
|
||||
*/
|
||||
data class BodyAdjustment(val startIndex: Int, val oldLength: Int, val newLength: Int)
|
||||
@@ -0,0 +1,37 @@
|
||||
@file:JvmName("BodyRangeUtil")
|
||||
|
||||
package org.thoughtcrime.securesms.database
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
|
||||
/**
|
||||
* Given a list of body adjustment from removing mention names from a message and replacing
|
||||
* them with a placeholder, we need to adjust the ranges within [BodyRangeList] to account
|
||||
* for the now shorter text.
|
||||
*/
|
||||
fun BodyRangeList?.adjustBodyRanges(bodyAdjustments: List<BodyAdjustment>): BodyRangeList? {
|
||||
if (this == null || bodyAdjustments.isEmpty()) {
|
||||
return this
|
||||
}
|
||||
|
||||
val newBodyRanges = rangesList.toMutableList()
|
||||
|
||||
for (adjustment in bodyAdjustments) {
|
||||
val adjustmentLength = adjustment.oldLength - adjustment.newLength
|
||||
|
||||
rangesList.forEachIndexed { listIndex, range ->
|
||||
val needsRangeStartsAfterAdjustment = range.start > adjustment.startIndex
|
||||
val needsRangeCoversAdjustment = range.start <= adjustment.startIndex && range.start + range.length >= adjustment.startIndex + adjustment.oldLength
|
||||
|
||||
val newRange = newBodyRanges[listIndex]
|
||||
val newStart: Int? = if (needsRangeStartsAfterAdjustment) newRange.start - adjustmentLength else null
|
||||
val newLength: Int? = if (needsRangeCoversAdjustment) newRange.length - adjustmentLength else null
|
||||
|
||||
if (newStart != null || newLength != null) {
|
||||
newBodyRanges[listIndex] = newRange.toBuilder().setStart(newStart ?: newRange.start).setLength(newLength ?: newRange.length).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return BodyRangeList.newBuilder().addAllRanges(newBodyRanges).build()
|
||||
}
|
||||
@@ -139,7 +139,7 @@ class DraftTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
const val AUDIO = "audio"
|
||||
const val LOCATION = "location"
|
||||
const val QUOTE = "quote"
|
||||
const val MENTION = "mention"
|
||||
const val BODY_RANGES = "mention"
|
||||
const val VOICE_NOTE = "voice_note"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,7 @@ import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
import com.annimon.stream.function.Function;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
@@ -35,25 +32,14 @@ public final class MentionUtil {
|
||||
private MentionUtil() { }
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull CharSequence updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord) {
|
||||
return updateBodyWithDisplayNames(context, messageRecord, messageRecord.getDisplayBody(context));
|
||||
public static @Nullable CharSequence updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord) {
|
||||
return updateBodyWithDisplayNames(context, messageRecord, messageRecord.getDisplayBody(context)).getBody();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull CharSequence updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body) {
|
||||
if (messageRecord.isMms()) {
|
||||
List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId());
|
||||
CharSequence updated = updateBodyAndMentionsWithDisplayNames(context, body, mentions).getBody();
|
||||
if (updated != null) {
|
||||
return updated;
|
||||
}
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull UpdatedBodyAndMentions updateBodyAndMentionsWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull List<Mention> mentions) {
|
||||
return updateBodyAndMentionsWithDisplayNames(context, messageRecord.getDisplayBody(context), mentions);
|
||||
public static @NonNull UpdatedBodyAndMentions updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body) {
|
||||
List<Mention> mentions = SignalDatabase.mentions().getMentionsForMessage(messageRecord.getId());
|
||||
return updateBodyAndMentionsWithDisplayNames(context, body, mentions);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
@@ -68,12 +54,13 @@ public final class MentionUtil {
|
||||
@VisibleForTesting
|
||||
static @NonNull UpdatedBodyAndMentions update(@Nullable CharSequence body, @NonNull List<Mention> mentions, @NonNull Function<Mention, CharSequence> replacementTextGenerator) {
|
||||
if (body == null || mentions.isEmpty()) {
|
||||
return new UpdatedBodyAndMentions(body, mentions);
|
||||
return new UpdatedBodyAndMentions(body, mentions, Collections.emptyList());
|
||||
}
|
||||
|
||||
SortedSet<Mention> sortedMentions = new TreeSet<>(mentions);
|
||||
SpannableStringBuilder updatedBody = new SpannableStringBuilder();
|
||||
List<Mention> updatedMentions = new ArrayList<>();
|
||||
List<BodyAdjustment> bodyAdjustments = new ArrayList<>();
|
||||
|
||||
int bodyIndex = 0;
|
||||
|
||||
@@ -89,6 +76,8 @@ public final class MentionUtil {
|
||||
updatedBody.append(replaceWith);
|
||||
updatedMentions.add(updatedMention);
|
||||
|
||||
bodyAdjustments.add(new BodyAdjustment(mention.getStart(), mention.getLength(), updatedMention.getLength()));
|
||||
|
||||
bodyIndex = mention.getStart() + mention.getLength();
|
||||
}
|
||||
|
||||
@@ -96,7 +85,7 @@ public final class MentionUtil {
|
||||
updatedBody.append(body.subSequence(bodyIndex, body.length()));
|
||||
}
|
||||
|
||||
return new UpdatedBodyAndMentions(updatedBody.toString(), updatedMentions);
|
||||
return new UpdatedBodyAndMentions(updatedBody, updatedMentions, bodyAdjustments);
|
||||
}
|
||||
|
||||
public static @Nullable BodyRangeList mentionsToBodyRangeList(@Nullable List<Mention> mentions) {
|
||||
@@ -117,34 +106,20 @@ public final class MentionUtil {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static @NonNull List<Mention> bodyRangeListToMentions(@NonNull Context context, @Nullable byte[] data) {
|
||||
if (data != null) {
|
||||
try {
|
||||
return Stream.of(BodyRangeList.parseFrom(data).getRangesList())
|
||||
.filter(bodyRange -> bodyRange.getAssociatedValueCase() == BodyRangeList.BodyRange.AssociatedValueCase.MENTIONUUID)
|
||||
.map(mention -> {
|
||||
RecipientId id = Recipient.externalPush(ServiceId.parseOrThrow(mention.getMentionUuid())).getId();
|
||||
return new Mention(id, mention.getStart(), mention.getLength());
|
||||
})
|
||||
.toList();
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
public static @NonNull List<Mention> bodyRangeListToMentions(@Nullable BodyRangeList bodyRanges) {
|
||||
if (bodyRanges != null) {
|
||||
return Stream.of(bodyRanges.getRangesList())
|
||||
.filter(bodyRange -> bodyRange.getAssociatedValueCase() == BodyRangeList.BodyRange.AssociatedValueCase.MENTIONUUID)
|
||||
.map(mention -> {
|
||||
RecipientId id = Recipient.externalPush(ServiceId.parseOrThrow(mention.getMentionUuid())).getId();
|
||||
return new Mention(id, mention.getStart(), mention.getLength());
|
||||
})
|
||||
.toList();
|
||||
} else {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public static @NonNull String getMentionSettingDisplayValue(@NonNull Context context, @NonNull MentionSetting mentionSetting) {
|
||||
switch (mentionSetting) {
|
||||
case ALWAYS_NOTIFY:
|
||||
return context.getString(R.string.GroupMentionSettingDialog_always_notify_me);
|
||||
case DO_NOT_NOTIFY:
|
||||
return context.getString(R.string.GroupMentionSettingDialog_dont_notify_me);
|
||||
}
|
||||
throw new IllegalArgumentException("Unknown mention setting: " + mentionSetting);
|
||||
}
|
||||
|
||||
private static boolean invalidMention(@NonNull CharSequence body, @NonNull Mention mention) {
|
||||
int start = mention.getStart();
|
||||
int length = mention.getLength();
|
||||
@@ -153,12 +128,14 @@ public final class MentionUtil {
|
||||
}
|
||||
|
||||
public static class UpdatedBodyAndMentions {
|
||||
@Nullable private final CharSequence body;
|
||||
@NonNull private final List<Mention> mentions;
|
||||
@Nullable private final CharSequence body;
|
||||
@NonNull private final List<Mention> mentions;
|
||||
@NonNull private final List<BodyAdjustment> bodyAdjustments;
|
||||
|
||||
public UpdatedBodyAndMentions(@Nullable CharSequence body, @NonNull List<Mention> mentions) {
|
||||
this.body = body;
|
||||
this.mentions = mentions;
|
||||
private UpdatedBodyAndMentions(@Nullable CharSequence body, @NonNull List<Mention> mentions, @NonNull List<BodyAdjustment> bodyAdjustments) {
|
||||
this.body = body;
|
||||
this.mentions = mentions;
|
||||
this.bodyAdjustments = bodyAdjustments;
|
||||
}
|
||||
|
||||
public @Nullable CharSequence getBody() {
|
||||
@@ -169,6 +146,10 @@ public final class MentionUtil {
|
||||
return mentions;
|
||||
}
|
||||
|
||||
public @NonNull List<BodyAdjustment> getBodyAdjustments() {
|
||||
return bodyAdjustments;
|
||||
}
|
||||
|
||||
@Nullable String getBodyAsString() {
|
||||
return body != null ? body.toString() : null;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ package org.thoughtcrime.securesms.database;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -47,6 +48,7 @@ import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.conversation.MessageStyler;
|
||||
import org.thoughtcrime.securesms.database.documents.Document;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchSet;
|
||||
@@ -169,7 +171,7 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
||||
public static final String QUOTE_AUTHOR = "quote_author";
|
||||
public static final String QUOTE_BODY = "quote_body";
|
||||
public static final String QUOTE_MISSING = "quote_missing";
|
||||
public static final String QUOTE_MENTIONS = "quote_mentions";
|
||||
public static final String QUOTE_BODY_RANGES = "quote_mentions";
|
||||
public static final String QUOTE_TYPE = "quote_type";
|
||||
public static final String SHARED_CONTACTS = "shared_contacts";
|
||||
public static final String LINK_PREVIEWS = "link_previews";
|
||||
@@ -216,7 +218,7 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
||||
QUOTE_AUTHOR + " INTEGER DEFAULT 0, " +
|
||||
QUOTE_BODY + " TEXT DEFAULT NULL, " +
|
||||
QUOTE_MISSING + " INTEGER DEFAULT 0, " +
|
||||
QUOTE_MENTIONS + " BLOB DEFAULT NULL," +
|
||||
QUOTE_BODY_RANGES + " BLOB DEFAULT NULL," +
|
||||
QUOTE_TYPE + " INTEGER DEFAULT 0," +
|
||||
SHARED_CONTACTS + " TEXT DEFAULT NULL, " +
|
||||
UNIDENTIFIED + " INTEGER DEFAULT 0, " +
|
||||
@@ -281,7 +283,7 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
||||
QUOTE_BODY,
|
||||
QUOTE_TYPE,
|
||||
QUOTE_MISSING,
|
||||
QUOTE_MENTIONS,
|
||||
QUOTE_BODY_RANGES,
|
||||
SHARED_CONTACTS,
|
||||
LINK_PREVIEWS,
|
||||
UNIDENTIFIED,
|
||||
@@ -2188,6 +2190,7 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
||||
String networkDocument = cursor.getString(cursor.getColumnIndexOrThrow(MessageTable.NETWORK_FAILURES));
|
||||
StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE));
|
||||
ParentStoryId parentStoryId = ParentStoryId.deserialize(CursorUtil.requireLong(cursor, PARENT_STORY_ID));
|
||||
byte[] messageRangesData = CursorUtil.requireBlob(cursor, MESSAGE_RANGES);
|
||||
|
||||
long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID));
|
||||
long quoteAuthor = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR));
|
||||
@@ -2195,7 +2198,8 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
||||
int quoteType = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE_TYPE));
|
||||
boolean quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE_MISSING)) == 1;
|
||||
List<Attachment> quoteAttachments = Stream.of(associatedAttachments).filter(Attachment::isQuote).map(a -> (Attachment)a).toList();
|
||||
List<Mention> quoteMentions = parseQuoteMentions(context, cursor);
|
||||
List<Mention> quoteMentions = parseQuoteMentions(cursor);
|
||||
BodyRangeList quoteBodyRanges = parseQuoteBodyRanges(cursor);
|
||||
List<Contact> contacts = getSharedContacts(cursor, associatedAttachments);
|
||||
Set<Attachment> contactAttachments = new HashSet<>(Stream.of(contacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList());
|
||||
List<LinkPreview> previews = getLinkPreviews(cursor, associatedAttachments);
|
||||
@@ -2212,7 +2216,7 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
||||
QuoteModel quote = null;
|
||||
|
||||
if (quoteId > 0 && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || !quoteAttachments.isEmpty())) {
|
||||
quote = new QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteAttachments, quoteMentions, QuoteModel.Type.fromCode(quoteType));
|
||||
quote = new QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteAttachments, quoteMentions, QuoteModel.Type.fromCode(quoteType), quoteBodyRanges);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(mismatchDocument)) {
|
||||
@@ -2248,6 +2252,15 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
||||
giftBadge = GiftBadge.parseFrom(Base64.decode(body));
|
||||
}
|
||||
|
||||
BodyRangeList messageRanges = null;
|
||||
if (messageRangesData != null) {
|
||||
try {
|
||||
messageRanges = BodyRangeList.parseFrom(messageRangesData);
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "Error parsing message ranges", e);
|
||||
}
|
||||
}
|
||||
|
||||
OutgoingMessage message = new OutgoingMessage(recipient,
|
||||
body,
|
||||
attachments,
|
||||
@@ -2266,7 +2279,8 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
||||
networkFailures,
|
||||
mismatches,
|
||||
giftBadge,
|
||||
MessageTypes.isSecureType(outboxType));
|
||||
MessageTypes.isSecureType(outboxType),
|
||||
messageRanges);
|
||||
|
||||
return message;
|
||||
}
|
||||
@@ -2398,14 +2412,21 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
||||
|
||||
if (retrieved.getQuote() != null) {
|
||||
contentValues.put(QUOTE_ID, retrieved.getQuote().getId());
|
||||
contentValues.put(QUOTE_BODY, retrieved.getQuote().getText().toString());
|
||||
contentValues.put(QUOTE_BODY, retrieved.getQuote().getText());
|
||||
contentValues.put(QUOTE_AUTHOR, retrieved.getQuote().getAuthor().serialize());
|
||||
contentValues.put(QUOTE_TYPE, retrieved.getQuote().getType().getCode());
|
||||
contentValues.put(QUOTE_MISSING, retrieved.getQuote().isOriginalMissing() ? 1 : 0);
|
||||
|
||||
BodyRangeList.Builder quoteBodyRanges = retrieved.getQuote().getBodyRanges() != null ? retrieved.getQuote().getBodyRanges().toBuilder()
|
||||
: BodyRangeList.newBuilder();
|
||||
|
||||
BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(retrieved.getQuote().getMentions());
|
||||
if (mentionsList != null) {
|
||||
contentValues.put(QUOTE_MENTIONS, mentionsList.toByteArray());
|
||||
quoteBodyRanges.addAllRanges(mentionsList.getRangesList());
|
||||
}
|
||||
|
||||
if (quoteBodyRanges.getRangesCount() > 0) {
|
||||
contentValues.put(QUOTE_BODY_RANGES, quoteBodyRanges.build().toByteArray());
|
||||
}
|
||||
|
||||
quoteAttachments = retrieved.getQuote().getAttachments();
|
||||
@@ -2789,17 +2810,30 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
||||
contentValues.put(QUOTE_TYPE, message.getOutgoingQuote().getType().getCode());
|
||||
contentValues.put(QUOTE_MISSING, message.getOutgoingQuote().isOriginalMissing() ? 1 : 0);
|
||||
|
||||
BodyRangeList adjustedQuoteBodyRanges = BodyRangeUtil.adjustBodyRanges(message.getOutgoingQuote().getBodyRanges(), updated.getBodyAdjustments());
|
||||
BodyRangeList.Builder quoteBodyRanges;
|
||||
if (adjustedQuoteBodyRanges != null) {
|
||||
quoteBodyRanges = adjustedQuoteBodyRanges.toBuilder();
|
||||
} else {
|
||||
quoteBodyRanges = BodyRangeList.newBuilder();
|
||||
}
|
||||
|
||||
BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(updated.getMentions());
|
||||
if (mentionsList != null) {
|
||||
contentValues.put(QUOTE_MENTIONS, mentionsList.toByteArray());
|
||||
quoteBodyRanges.addAllRanges(mentionsList.getRangesList());
|
||||
}
|
||||
|
||||
if (quoteBodyRanges.getRangesCount() > 0) {
|
||||
contentValues.put(QUOTE_BODY_RANGES, quoteBodyRanges.build().toByteArray());
|
||||
}
|
||||
|
||||
quoteAttachments.addAll(message.getOutgoingQuote().getAttachments());
|
||||
}
|
||||
|
||||
MentionUtil.UpdatedBodyAndMentions updatedBodyAndMentions = MentionUtil.updateBodyAndMentionsWithPlaceholders(message.getBody(), message.getMentions());
|
||||
BodyRangeList bodyRanges = BodyRangeUtil.adjustBodyRanges(message.getBodyRanges(), updatedBodyAndMentions.getBodyAdjustments());
|
||||
|
||||
long messageId = insertMediaMessage(threadId, updatedBodyAndMentions.getBodyAsString(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), updatedBodyAndMentions.getMentions(), null, contentValues, insertListener, false, false);
|
||||
long messageId = insertMediaMessage(threadId, updatedBodyAndMentions.getBodyAsString(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), updatedBodyAndMentions.getMentions(), bodyRanges, contentValues, insertListener, false, false);
|
||||
|
||||
if (message.getRecipient().isGroup()) {
|
||||
GroupReceiptTable receiptDatabase = SignalDatabase.groupReceipts();
|
||||
@@ -3281,10 +3315,37 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull List<Mention> parseQuoteMentions(@NonNull Context context, Cursor cursor) {
|
||||
byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(QUOTE_MENTIONS));
|
||||
private static @NonNull List<Mention> parseQuoteMentions(@NonNull Cursor cursor) {
|
||||
byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(QUOTE_BODY_RANGES));
|
||||
BodyRangeList bodyRanges = null;
|
||||
|
||||
return MentionUtil.bodyRangeListToMentions(context, raw);
|
||||
if (raw != null) {
|
||||
try {
|
||||
bodyRanges = BodyRangeList.parseFrom(raw);
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "Unable to parse quote body ranges", e);
|
||||
}
|
||||
}
|
||||
|
||||
return MentionUtil.bodyRangeListToMentions(bodyRanges);
|
||||
}
|
||||
|
||||
private static @Nullable BodyRangeList parseQuoteBodyRanges(@NonNull Cursor cursor) {
|
||||
byte[] data = cursor.getBlob(cursor.getColumnIndexOrThrow(QUOTE_BODY_RANGES));
|
||||
|
||||
if (data != null) {
|
||||
try {
|
||||
final List<BodyRangeList.BodyRange> bodyRanges = Stream.of(BodyRangeList.parseFrom(data).getRangesList())
|
||||
.filter(bodyRange -> bodyRange.getAssociatedValueCase() != BodyRangeList.BodyRange.AssociatedValueCase.MENTIONUUID)
|
||||
.toList();
|
||||
|
||||
return BodyRangeList.newBuilder().addAllRanges(bodyRanges).build();
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
// Intentionally left blank
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public SQLiteDatabase beginTransaction() {
|
||||
@@ -4574,6 +4635,32 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull Map<Long, BodyRangeList> getBodyRangesForMessages(@NonNull List<Long> messageIds) {
|
||||
List<SqlUtil.Query> queries = SqlUtil.buildCollectionQuery(ID, messageIds);
|
||||
Map<Long, BodyRangeList> bodyRanges = new HashMap<>();
|
||||
|
||||
for (SqlUtil.Query query : queries) {
|
||||
try (Cursor cursor = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), ID, MESSAGE_RANGES)
|
||||
.from(TABLE_NAME)
|
||||
.where(query.getWhere(), query.getWhereArgs())
|
||||
.run())
|
||||
{
|
||||
while (cursor.moveToNext()) {
|
||||
byte[] data = CursorUtil.requireBlob(cursor, MESSAGE_RANGES);
|
||||
if (data != null) {
|
||||
try {
|
||||
bodyRanges.put(CursorUtil.requireLong(cursor, ID), BodyRangeList.parseFrom(data));
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "Unable to parse body ranges for search", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bodyRanges;
|
||||
}
|
||||
|
||||
protected enum ReceiptType {
|
||||
READ(READ_RECEIPT_COUNT, GroupReceiptTable.STATUS_READ),
|
||||
DELIVERY(DELIVERY_RECEIPT_COUNT, GroupReceiptTable.STATUS_DELIVERED),
|
||||
@@ -4922,13 +5009,17 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
||||
public MessageRecord getCurrent() {
|
||||
SlideDeck slideDeck = new SlideDeck(context, message.getAttachments());
|
||||
|
||||
CharSequence quoteText = message.getOutgoingQuote() != null ? message.getOutgoingQuote().getText() : null;
|
||||
List<Mention> quoteMentions = message.getOutgoingQuote() != null ? message.getOutgoingQuote().getMentions() : Collections.emptyList();
|
||||
CharSequence quoteText = message.getOutgoingQuote() != null ? message.getOutgoingQuote().getText() : null;
|
||||
List<Mention> quoteMentions = message.getOutgoingQuote() != null ? message.getOutgoingQuote().getMentions() : Collections.emptyList();
|
||||
BodyRangeList quoteBodyRanges = message.getOutgoingQuote() != null ? message.getOutgoingQuote().getBodyRanges() : null;
|
||||
|
||||
if (quoteText != null && !quoteMentions.isEmpty()) {
|
||||
if (quoteText != null && (Util.hasItems(quoteMentions) || quoteBodyRanges != null)) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions);
|
||||
|
||||
quoteText = updated.getBody();
|
||||
SpannableString styledText = new SpannableString(updated.getBody());
|
||||
MessageStyler.style(BodyRangeUtil.adjustBodyRanges(quoteBodyRanges, updated.getBodyAdjustments()), styledText);
|
||||
|
||||
quoteText = styledText;
|
||||
quoteMentions = updated.getMentions();
|
||||
}
|
||||
|
||||
@@ -5202,16 +5293,20 @@ public class MessageTable extends DatabaseTable implements MessageTypes, Recipie
|
||||
CharSequence quoteText = cursor.getString(cursor.getColumnIndexOrThrow(MessageTable.QUOTE_BODY));
|
||||
int quoteType = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.QUOTE_TYPE));
|
||||
boolean quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(MessageTable.QUOTE_MISSING)) == 1;
|
||||
List<Mention> quoteMentions = parseQuoteMentions(context, cursor);
|
||||
List<Mention> quoteMentions = parseQuoteMentions(cursor);
|
||||
BodyRangeList bodyRanges = parseQuoteBodyRanges(cursor);
|
||||
List<DatabaseAttachment> attachments = SignalDatabase.attachments().getAttachments(cursor);
|
||||
List<? extends Attachment> quoteAttachments = Stream.of(attachments).filter(Attachment::isQuote).toList();
|
||||
SlideDeck quoteDeck = new SlideDeck(context, quoteAttachments);
|
||||
|
||||
if (quoteId > 0 && quoteAuthor > 0) {
|
||||
if (quoteText != null && !quoteMentions.isEmpty()) {
|
||||
if (quoteText != null && (Util.hasItems(quoteMentions) || bodyRanges != null)) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions);
|
||||
|
||||
quoteText = updated.getBody();
|
||||
SpannableString styledText = new SpannableString(updated.getBody());
|
||||
MessageStyler.style(BodyRangeUtil.adjustBodyRanges(bodyRanges, updated.getBodyAdjustments()), styledText);
|
||||
|
||||
quoteText = styledText;
|
||||
quoteMentions = updated.getMentions();
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@ import org.thoughtcrime.securesms.mms.StickerSlide;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class ThreadBodyUtil {
|
||||
@@ -28,19 +30,19 @@ public final class ThreadBodyUtil {
|
||||
private ThreadBodyUtil() {
|
||||
}
|
||||
|
||||
public static @NonNull String getFormattedBodyFor(@NonNull Context context, @NonNull MessageRecord record) {
|
||||
public static @NonNull ThreadBody getFormattedBodyFor(@NonNull Context context, @NonNull MessageRecord record) {
|
||||
if (record.isMms()) {
|
||||
return getFormattedBodyForMms(context, (MmsMessageRecord) record);
|
||||
}
|
||||
|
||||
return record.getBody();
|
||||
return new ThreadBody(record.getBody());
|
||||
}
|
||||
|
||||
private static @NonNull String getFormattedBodyForMms(@NonNull Context context, @NonNull MmsMessageRecord record) {
|
||||
private static @NonNull ThreadBody getFormattedBodyForMms(@NonNull Context context, @NonNull MmsMessageRecord record) {
|
||||
if (record.getSharedContacts().size() > 0) {
|
||||
Contact contact = record.getSharedContacts().get(0);
|
||||
|
||||
return ContactUtil.getStringSummary(context, contact).toString();
|
||||
return new ThreadBody(ContactUtil.getStringSummary(context, contact).toString());
|
||||
} else if (record.getSlideDeck().getDocumentSlide() != null) {
|
||||
return format(context, record, EmojiStrings.FILE, R.string.ThreadRecord_file);
|
||||
} else if (record.getSlideDeck().getAudioSlide() != null) {
|
||||
@@ -49,17 +51,17 @@ public final class ThreadBodyUtil {
|
||||
String emoji = getStickerEmoji(record);
|
||||
return format(context, record, emoji, R.string.ThreadRecord_sticker);
|
||||
} else if (MessageRecordUtil.hasGiftBadge(record)) {
|
||||
return String.format("%s %s", EmojiStrings.GIFT, getGiftSummary(context, record));
|
||||
return format(EmojiStrings.GIFT, getGiftSummary(context, record));
|
||||
} else if (MessageRecordUtil.isStoryReaction(record)) {
|
||||
return getStoryReactionSummary(context, record);
|
||||
return new ThreadBody(getStoryReactionSummary(context, record));
|
||||
} else if (record.isPaymentNotification()) {
|
||||
return String.format("%s %s", EmojiStrings.CARD, context.getString(R.string.ThreadRecord_payment));
|
||||
return format(EmojiStrings.CARD, context.getString(R.string.ThreadRecord_payment));
|
||||
} else if (record.isPaymentsRequestToActivate()) {
|
||||
return String.format("%s %s", EmojiStrings.CARD, getPaymentActivationRequestSummary(context, record));
|
||||
return format(EmojiStrings.CARD, getPaymentActivationRequestSummary(context, record));
|
||||
} else if (record.isPaymentsActivated()) {
|
||||
return String.format("%s %s", EmojiStrings.CARD, getPaymentActivatedSummary(context, record));
|
||||
return format(EmojiStrings.CARD, getPaymentActivatedSummary(context, record));
|
||||
} else if (record.isCallLog() && !record.isGroupCall()) {
|
||||
return getCallLogSummary(context, record);
|
||||
return new ThreadBody(getCallLogSummary(context, record));
|
||||
}
|
||||
|
||||
boolean hasImage = false;
|
||||
@@ -79,7 +81,7 @@ public final class ThreadBodyUtil {
|
||||
} else if (hasImage) {
|
||||
return format(context, record, EmojiStrings.PHOTO, R.string.ThreadRecord_photo);
|
||||
} else if (TextUtils.isEmpty(record.getBody())) {
|
||||
return context.getString(R.string.ThreadRecord_media_message);
|
||||
return new ThreadBody(context.getString(R.string.ThreadRecord_media_message));
|
||||
} else {
|
||||
return getBody(context, record);
|
||||
}
|
||||
@@ -146,16 +148,23 @@ public final class ThreadBodyUtil {
|
||||
}
|
||||
}
|
||||
|
||||
private static @NonNull String format(@NonNull Context context, @NonNull MessageRecord record, @NonNull String emoji, @StringRes int defaultStringRes) {
|
||||
return String.format("%s %s", emoji, getBodyOrDefault(context, record, defaultStringRes));
|
||||
private static @NonNull ThreadBody format(@NonNull Context context, @NonNull MessageRecord record, @NonNull String emoji, @StringRes int defaultStringRes) {
|
||||
CharSequence body = getBodyOrDefault(context, record, defaultStringRes).getBody();
|
||||
return format(emoji, body);
|
||||
}
|
||||
|
||||
private static @NonNull String getBodyOrDefault(@NonNull Context context, @NonNull MessageRecord record, @StringRes int defaultStringRes) {
|
||||
return TextUtils.isEmpty(record.getBody()) ? context.getString(defaultStringRes) : getBody(context, record);
|
||||
private static @NonNull ThreadBody format(@NonNull CharSequence prefix, @NonNull CharSequence body) {
|
||||
return new ThreadBody(String.format("%s %s", prefix, body), prefix.length() + 1);
|
||||
}
|
||||
|
||||
private static @NonNull String getBody(@NonNull Context context, @NonNull MessageRecord record) {
|
||||
return MentionUtil.updateBodyWithDisplayNames(context, record, record.getBody()).toString();
|
||||
private static @NonNull ThreadBody getBodyOrDefault(@NonNull Context context, @NonNull MessageRecord record, @StringRes int defaultStringRes) {
|
||||
return TextUtils.isEmpty(record.getBody()) ? new ThreadBody(context.getString(defaultStringRes)) : getBody(context, record);
|
||||
}
|
||||
|
||||
private static @NonNull ThreadBody getBody(@NonNull Context context, @NonNull MessageRecord record) {
|
||||
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyWithDisplayNames(context, record, record.getBody());
|
||||
//noinspection ConstantConditions
|
||||
return new ThreadBody(updated.getBody(), updated.getBodyAdjustments());
|
||||
}
|
||||
|
||||
private static @NonNull String getStickerEmoji(@NonNull MessageRecord record) {
|
||||
@@ -164,4 +173,30 @@ public final class ThreadBodyUtil {
|
||||
return Util.isEmpty(slide.getEmoji()) ? EmojiStrings.STICKER
|
||||
: slide.getEmoji();
|
||||
}
|
||||
|
||||
public static class ThreadBody {
|
||||
private final CharSequence body;
|
||||
private final List<BodyAdjustment> bodyAdjustments;
|
||||
|
||||
public ThreadBody(@NonNull CharSequence body) {
|
||||
this(body, 0);
|
||||
}
|
||||
|
||||
public ThreadBody(@NonNull CharSequence body, int startOffset) {
|
||||
this(body, startOffset == 0 ? Collections.emptyList() : Collections.singletonList(new BodyAdjustment(0, 0, startOffset)));
|
||||
}
|
||||
|
||||
public ThreadBody(@NonNull CharSequence body, @NonNull List<BodyAdjustment> bodyAdjustments) {
|
||||
this.body = body;
|
||||
this.bodyAdjustments = bodyAdjustments;
|
||||
}
|
||||
|
||||
public @NonNull CharSequence getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public @NonNull List<BodyAdjustment> getBodyAdjustments() {
|
||||
return bodyAdjustments;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,10 +36,13 @@ import org.thoughtcrime.securesms.database.SignalDatabase.Companion.mentions
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messageLog
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.messages
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients
|
||||
import org.thoughtcrime.securesms.database.ThreadBodyUtil.ThreadBody
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.database.model.serialize
|
||||
import org.thoughtcrime.securesms.groups.BadGroupIdException
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
@@ -1376,13 +1379,15 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
}
|
||||
}
|
||||
|
||||
val threadBody: ThreadBody = ThreadBodyUtil.getFormattedBodyFor(context, record)
|
||||
|
||||
updateThread(
|
||||
threadId = threadId,
|
||||
meaningfulMessages = meaningfulMessages,
|
||||
body = ThreadBodyUtil.getFormattedBodyFor(context, record),
|
||||
body = threadBody.body.toString(),
|
||||
attachment = getAttachmentUriFor(record),
|
||||
contentType = getContentTypeFor(record),
|
||||
extra = getExtrasFor(record),
|
||||
extra = getExtrasFor(record, threadBody),
|
||||
date = record.timestamp,
|
||||
status = record.deliveryStatus,
|
||||
deliveryReceiptCount = record.deliveryReceiptCount,
|
||||
@@ -1534,7 +1539,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getExtrasFor(record: MessageRecord): Extra? {
|
||||
private fun getExtrasFor(record: MessageRecord, body: ThreadBody): Extra? {
|
||||
val threadRecipient = if (record.isOutgoing) record.recipient else getRecipientForThreadId(record.threadId)
|
||||
val messageRequestAccepted = RecipientUtil.isMessageRequestAccepted(record.threadId, threadRecipient)
|
||||
val individualRecipientId = record.individualRecipient.id
|
||||
@@ -1567,7 +1572,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
}
|
||||
}
|
||||
|
||||
return if (record.isRemoteDelete) {
|
||||
val extras: Extra? = if (record.isRemoteDelete) {
|
||||
Extra.forRemoteDelete(individualRecipientId)
|
||||
} else if (record.isViewOnce) {
|
||||
Extra.forViewOnce(individualRecipientId)
|
||||
@@ -1581,6 +1586,13 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
return if (record.messageRanges != null) {
|
||||
val bodyRanges = record.requireMessageRanges().adjustBodyRanges(body.bodyAdjustments)!!
|
||||
extras?.copy(bodyRanges = bodyRanges.serialize()) ?: Extra.forBodyRanges(bodyRanges, individualRecipientId)
|
||||
} else {
|
||||
extras
|
||||
}
|
||||
}
|
||||
|
||||
private fun createQuery(where: String, limit: Long): String {
|
||||
@@ -1754,15 +1766,16 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
}
|
||||
|
||||
data class Extra(
|
||||
@field:JsonProperty @param:JsonProperty("isRevealable") val isViewOnce: Boolean,
|
||||
@field:JsonProperty @param:JsonProperty("isSticker") val isSticker: Boolean,
|
||||
@field:JsonProperty @param:JsonProperty("stickerEmoji") val stickerEmoji: String?,
|
||||
@field:JsonProperty @param:JsonProperty("isAlbum") val isAlbum: Boolean,
|
||||
@field:JsonProperty @param:JsonProperty("isRemoteDelete") val isRemoteDelete: Boolean,
|
||||
@field:JsonProperty @param:JsonProperty("isMessageRequestAccepted") val isMessageRequestAccepted: Boolean,
|
||||
@field:JsonProperty @param:JsonProperty("isGv2Invite") val isGv2Invite: Boolean,
|
||||
@field:JsonProperty @param:JsonProperty("groupAddedBy") val groupAddedBy: String?,
|
||||
@field:JsonProperty @param:JsonProperty("individualRecipientId") private val individualRecipientId: String
|
||||
@field:JsonProperty @param:JsonProperty("isRevealable") val isViewOnce: Boolean = false,
|
||||
@field:JsonProperty @param:JsonProperty("isSticker") val isSticker: Boolean = false,
|
||||
@field:JsonProperty @param:JsonProperty("stickerEmoji") val stickerEmoji: String? = null,
|
||||
@field:JsonProperty @param:JsonProperty("isAlbum") val isAlbum: Boolean = false,
|
||||
@field:JsonProperty @param:JsonProperty("isRemoteDelete") val isRemoteDelete: Boolean = false,
|
||||
@field:JsonProperty @param:JsonProperty("isMessageRequestAccepted") val isMessageRequestAccepted: Boolean = true,
|
||||
@field:JsonProperty @param:JsonProperty("isGv2Invite") val isGv2Invite: Boolean = false,
|
||||
@field:JsonProperty @param:JsonProperty("groupAddedBy") val groupAddedBy: String? = null,
|
||||
@field:JsonProperty @param:JsonProperty("individualRecipientId") private val individualRecipientId: String,
|
||||
@field:JsonProperty @param:JsonProperty("bodyRanges") val bodyRanges: String? = null
|
||||
) {
|
||||
|
||||
fun getIndividualRecipientId(): String {
|
||||
@@ -1771,35 +1784,39 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
|
||||
companion object {
|
||||
fun forViewOnce(individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = true, isSticker = false, stickerEmoji = null, isAlbum = false, isRemoteDelete = false, isMessageRequestAccepted = true, isGv2Invite = false, groupAddedBy = null, individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(isViewOnce = true, individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forSticker(emoji: String?, individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = false, isSticker = true, stickerEmoji = emoji, isAlbum = false, isRemoteDelete = false, isMessageRequestAccepted = true, isGv2Invite = false, groupAddedBy = null, individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(isSticker = true, stickerEmoji = emoji, individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forAlbum(individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = false, isSticker = false, stickerEmoji = null, isAlbum = true, isRemoteDelete = false, isMessageRequestAccepted = true, isGv2Invite = false, groupAddedBy = null, individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(isAlbum = true, individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forRemoteDelete(individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = false, isSticker = false, stickerEmoji = null, isAlbum = false, isRemoteDelete = true, isMessageRequestAccepted = true, isGv2Invite = false, groupAddedBy = null, individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(isRemoteDelete = true, individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forMessageRequest(individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = false, isSticker = false, stickerEmoji = null, isAlbum = false, isRemoteDelete = false, isMessageRequestAccepted = false, isGv2Invite = false, groupAddedBy = null, individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(isMessageRequestAccepted = false, individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forGroupMessageRequest(recipientId: RecipientId, individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = false, isSticker = false, stickerEmoji = null, isAlbum = false, isRemoteDelete = false, isMessageRequestAccepted = false, isGv2Invite = false, groupAddedBy = recipientId.serialize(), individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(isMessageRequestAccepted = false, groupAddedBy = recipientId.serialize(), individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forGroupV2invite(recipientId: RecipientId, individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = false, isSticker = false, stickerEmoji = null, isAlbum = false, isRemoteDelete = false, isMessageRequestAccepted = false, isGv2Invite = true, groupAddedBy = recipientId.serialize(), individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(isGv2Invite = true, groupAddedBy = recipientId.serialize(), individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forDefault(individualRecipient: RecipientId): Extra {
|
||||
return Extra(isViewOnce = false, isSticker = false, stickerEmoji = null, isAlbum = false, isRemoteDelete = false, isMessageRequestAccepted = true, isGv2Invite = false, groupAddedBy = null, individualRecipientId = individualRecipient.serialize())
|
||||
return Extra(individualRecipientId = individualRecipient.serialize())
|
||||
}
|
||||
|
||||
fun forBodyRanges(bodyRanges: BodyRangeList, individualRecipient: RecipientId): Extra {
|
||||
return Extra(individualRecipientId = individualRecipient.serialize(), bodyRanges = bodyRanges.serialize())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import org.signal.core.util.StringSerializer
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
|
||||
object BodyRangeListSerializer : StringSerializer<BodyRangeList> {
|
||||
override fun serialize(data: BodyRangeList): String = Base64.encodeBytes(data.toByteArray())
|
||||
override fun deserialize(data: String): BodyRangeList = BodyRangeList.parseFrom(Base64.decode(data))
|
||||
}
|
||||
|
||||
fun BodyRangeList.serialize(): String {
|
||||
return BodyRangeListSerializer.serialize(this)
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
@file:JvmName("DatabaseProtosUtil")
|
||||
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import com.google.protobuf.ByteString
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.BodyRange
|
||||
|
||||
/**
|
||||
* Collection of extensions to make working with database protos cleaner.
|
||||
@@ -43,3 +47,28 @@ fun BodyRangeList.Builder.addButton(label: String, action: String, start: Int, l
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
fun List<BodyRange>?.toBodyRangeList(): BodyRangeList? {
|
||||
if (this == null || !FeatureFlags.textFormatting()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val builder = BodyRangeList.newBuilder()
|
||||
|
||||
for (bodyRange in this) {
|
||||
var style: BodyRangeList.BodyRange.Style? = null
|
||||
when (bodyRange.style) {
|
||||
BodyRange.Style.BOLD -> style = BodyRangeList.BodyRange.Style.BOLD
|
||||
BodyRange.Style.ITALIC -> style = BodyRangeList.BodyRange.Style.ITALIC
|
||||
BodyRange.Style.SPOILER -> Unit
|
||||
BodyRange.Style.STRIKETHROUGH -> style = BodyRangeList.BodyRange.Style.STRIKETHROUGH
|
||||
BodyRange.Style.MONOSPACE -> style = BodyRangeList.BodyRange.Style.MONOSPACE
|
||||
else -> Unit
|
||||
}
|
||||
if (style != null) {
|
||||
builder.addStyle(style, bodyRange.start, bodyRange.length)
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build()
|
||||
}
|
||||
@@ -179,15 +179,11 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
return super.getUpdateDisplayBody(context, recipientClickHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable BodyRangeList getMessageRanges() {
|
||||
return messageRanges;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMessageRanges() {
|
||||
return messageRanges != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull BodyRangeList requireMessageRanges() {
|
||||
return Objects.requireNonNull(messageRanges);
|
||||
|
||||
@@ -703,8 +703,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return isJumboji;
|
||||
}
|
||||
|
||||
public boolean hasMessageRanges() {
|
||||
return false;
|
||||
public @Nullable BodyRangeList getMessageRanges() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public @NonNull BodyRangeList requireMessageRanges() {
|
||||
|
||||
@@ -38,7 +38,7 @@ public class Quote {
|
||||
this.mentions = mentions;
|
||||
this.quoteType = quoteType;
|
||||
|
||||
SpannableString spannable = new SpannableString(Util.emptyIfNull(text));
|
||||
SpannableString spannable = SpannableString.valueOf(Util.emptyIfNull(text));
|
||||
MentionAnnotation.setMentionAnnotations(spannable, mentions);
|
||||
|
||||
this.text = spannable;
|
||||
@@ -48,7 +48,6 @@ public class Quote {
|
||||
return new Quote(id, author, text, missing, updatedAttachment, mentions, quoteType);
|
||||
}
|
||||
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import androidx.annotation.Nullable;
|
||||
import org.thoughtcrime.securesms.database.MessageTypes;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable.Extra;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.signalservice.api.util.Preconditions;
|
||||
@@ -100,6 +101,14 @@ public final class ThreadRecord {
|
||||
return extra;
|
||||
}
|
||||
|
||||
public @Nullable BodyRangeList getBodyRanges() {
|
||||
if (extra != null && extra.getBodyRanges() != null) {
|
||||
return BodyRangeListSerializer.INSTANCE.deserialize(extra.getBodyRanges());
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable String getContentType() {
|
||||
return contentType;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user