Add mentions for v2 group chats.

This commit is contained in:
Cody Henthorne
2020-08-05 16:45:52 -04:00
committed by Greyson Parrelli
parent 0bb9c1d650
commit b2d4c5d14b
90 changed files with 2279 additions and 372 deletions

View File

@@ -63,6 +63,7 @@ public class DatabaseFactory {
private final KeyValueDatabase keyValueDatabase;
private final MegaphoneDatabase megaphoneDatabase;
private final RemappedRecordsDatabase remappedRecordsDatabase;
private final MentionDatabase mentionDatabase;
public static DatabaseFactory getInstance(Context context) {
synchronized (lock) {
@@ -165,6 +166,10 @@ public class DatabaseFactory {
return getInstance(context).remappedRecordsDatabase;
}
public static MentionDatabase getMentionDatabase(Context context) {
return getInstance(context).mentionDatabase;
}
public static SQLiteDatabase getBackupDatabase(Context context) {
return getInstance(context).databaseHelper.getReadableDatabase();
}
@@ -214,6 +219,7 @@ public class DatabaseFactory {
this.keyValueDatabase = new KeyValueDatabase(context, databaseHelper);
this.megaphoneDatabase = new MegaphoneDatabase(context, databaseHelper);
this.remappedRecordsDatabase = new RemappedRecordsDatabase(context, databaseHelper);
this.mentionDatabase = new MentionDatabase(context, databaseHelper);
}
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,

View File

@@ -4,6 +4,8 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import net.sqlcipher.database.SQLiteDatabase;
@@ -73,14 +75,11 @@ public class DraftDatabase extends Database {
db.delete(TABLE_NAME, null, null);
}
public List<Draft> getDrafts(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
List<Draft> results = new LinkedList<>();
Cursor cursor = null;
try {
cursor = db.query(TABLE_NAME, null, THREAD_ID + " = ?", new String[] {threadId+""}, null, null, null);
public Drafts getDrafts(long threadId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Drafts results = new Drafts();
try (Cursor cursor = db.query(TABLE_NAME, null, THREAD_ID + " = ?", new String[] {threadId+""}, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
String type = cursor.getString(cursor.getColumnIndexOrThrow(DRAFT_TYPE));
String value = cursor.getString(cursor.getColumnIndexOrThrow(DRAFT_VALUE));
@@ -89,9 +88,6 @@ public class DraftDatabase extends Database {
}
return results;
} finally {
if (cursor != null)
cursor.close();
}
}
@@ -102,6 +98,7 @@ public class DraftDatabase extends Database {
public static final String AUDIO = "audio";
public static final String LOCATION = "location";
public static final String QUOTE = "quote";
public static final String MENTION = "mention";
private final String type;
private final String value;
@@ -133,7 +130,7 @@ public class DraftDatabase extends Database {
}
public static class Drafts extends LinkedList<Draft> {
private Draft getDraftOfType(String type) {
public @Nullable Draft getDraftOfType(String type) {
for (Draft draft : this) {
if (type.equals(draft.getType())) {
return draft;
@@ -142,7 +139,7 @@ public class DraftDatabase extends Database {
return null;
}
public String getSnippet(Context context) {
public @NonNull String getSnippet(Context context) {
Draft textDraft = getDraftOfType(Draft.TEXT);
if (textDraft != null) {
return textDraft.getSnippet(context);

View File

@@ -0,0 +1,112 @@
package org.thoughtcrime.securesms.database;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import net.sqlcipher.database.SQLiteDatabase;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.SqlUtil;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class MentionDatabase extends Database {
private static final String TABLE_NAME = "mention";
private static final String ID = "_id";
private static final String THREAD_ID = "thread_id";
private static final String MESSAGE_ID = "message_id";
private static final String RECIPIENT_ID = "recipient_id";
private static final String RANGE_START = "range_start";
private static final String RANGE_LENGTH = "range_length";
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
THREAD_ID + " INTEGER, " +
MESSAGE_ID + " INTEGER, " +
RECIPIENT_ID + " INTEGER, " +
RANGE_START + " INTEGER, " +
RANGE_LENGTH + " INTEGER)";
public static final String[] CREATE_INDEXES = new String[] {
"CREATE INDEX IF NOT EXISTS mention_message_id_index ON " + TABLE_NAME + " (" + MESSAGE_ID + ");",
"CREATE INDEX IF NOT EXISTS mention_recipient_id_thread_id_index ON " + TABLE_NAME + " (" + RECIPIENT_ID + ", " + THREAD_ID + ");"
};
public MentionDatabase(@NonNull Context context, @NonNull SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
}
public void insert(long threadId, long messageId, @NonNull Collection<Mention> mentions) {
SQLiteDatabase db = databaseHelper.getWritableDatabase();
db.beginTransaction();
try {
for (Mention mention : mentions) {
ContentValues values = new ContentValues();
values.put(THREAD_ID, threadId);
values.put(MESSAGE_ID, messageId);
values.put(RECIPIENT_ID, mention.getRecipientId().toLong());
values.put(RANGE_START, mention.getStart());
values.put(RANGE_LENGTH, mention.getLength());
db.insert(TABLE_NAME, null, values);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
public @NonNull List<Mention> getMentionsForMessage(long messageId) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
List<Mention> mentions = new LinkedList<>();
try (Cursor cursor = db.query(TABLE_NAME, null, MESSAGE_ID + " = ?", SqlUtil.buildArgs(messageId), null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
mentions.add(new Mention(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)),
CursorUtil.requireInt(cursor, RANGE_START),
CursorUtil.requireInt(cursor, RANGE_LENGTH)));
}
}
return mentions;
}
public @NonNull Map<Long, List<Mention>> getMentionsForMessages(@NonNull Collection<Long> messageIds) {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
Map<Long, List<Mention>> mentions = new HashMap<>();
String ids = TextUtils.join(",", messageIds);
try (Cursor cursor = db.query(TABLE_NAME, null, MESSAGE_ID + " IN (" + ids + ")", null, null, null, null)) {
while (cursor != null && cursor.moveToNext()) {
long messageId = CursorUtil.requireLong(cursor, MESSAGE_ID);
List<Mention> messageMentions = mentions.get(messageId);
if (messageMentions == null) {
messageMentions = new LinkedList<>();
mentions.put(messageId, messageMentions);
}
messageMentions.add(new Mention(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)),
CursorUtil.requireInt(cursor, RANGE_START),
CursorUtil.requireInt(cursor, RANGE_LENGTH)));
}
}
return mentions;
}
}

View File

@@ -0,0 +1,166 @@
package org.thoughtcrime.securesms.database;
import android.content.Context;
import android.text.SpannableStringBuilder;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
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.RecipientDatabase.MentionSetting;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public final class MentionUtil {
public static final char MENTION_STARTER = '@';
static final String MENTION_PLACEHOLDER = "\uFFFC";
private MentionUtil() { }
@WorkerThread
public static @NonNull CharSequence updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord) {
return updateBodyWithDisplayNames(context, messageRecord, messageRecord.getDisplayBody(context));
}
@WorkerThread
public static @NonNull CharSequence updateBodyWithDisplayNames(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull CharSequence body) {
if (messageRecord.isMms()) {
List<Mention> mentions = DatabaseFactory.getMentionDatabase(context).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);
}
@WorkerThread
public static @NonNull UpdatedBodyAndMentions updateBodyAndMentionsWithDisplayNames(@NonNull Context context, @NonNull CharSequence body, @NonNull List<Mention> mentions) {
return update(body, mentions, m -> MENTION_STARTER + Recipient.resolved(m.getRecipientId()).getDisplayName(context));
}
public static @NonNull UpdatedBodyAndMentions updateBodyAndMentionsWithPlaceholders(@Nullable CharSequence body, @NonNull List<Mention> mentions) {
return update(body, mentions, m -> MENTION_PLACEHOLDER);
}
private 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);
}
SpannableStringBuilder updatedBody = new SpannableStringBuilder();
List<Mention> updatedMentions = new ArrayList<>();
Collections.sort(mentions);
int bodyIndex = 0;
for (Mention mention : mentions) {
updatedBody.append(body.subSequence(bodyIndex, mention.getStart()));
CharSequence replaceWith = replacementTextGenerator.apply(mention);
Mention updatedMention = new Mention(mention.getRecipientId(), updatedBody.length(), replaceWith.length());
updatedBody.append(replaceWith);
updatedMentions.add(updatedMention);
bodyIndex = mention.getStart() + mention.getLength();
}
if (bodyIndex < body.length()) {
updatedBody.append(body.subSequence(bodyIndex, body.length()));
}
return new UpdatedBodyAndMentions(updatedBody.toString(), updatedMentions);
}
public static @Nullable BodyRangeList mentionsToBodyRangeList(@Nullable List<Mention> mentions) {
if (mentions == null || mentions.isEmpty()) {
return null;
}
BodyRangeList.Builder builder = BodyRangeList.newBuilder();
for (Mention mention : mentions) {
String uuid = Recipient.resolved(mention.getRecipientId()).requireUuid().toString();
builder.addRanges(BodyRangeList.BodyRange.newBuilder()
.setMentionUuid(uuid)
.setStart(mention.getStart())
.setLength(mention.getLength()));
}
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(context, UuidUtil.parseOrThrow(mention.getMentionUuid()), null, false).getId();
return new Mention(id, mention.getStart(), mention.getLength());
})
.toList();
} catch (InvalidProtocolBufferException e) {
return Collections.emptyList();
}
} else {
return Collections.emptyList();
}
}
public static @NonNull String getMentionSettingDisplayValue(@NonNull Context context, @NonNull MentionSetting mentionSetting) {
switch (mentionSetting) {
case GLOBAL:
return context.getString(SignalStore.notificationSettings().isMentionNotifiesMeEnabled() ? R.string.GroupMentionSettingDialog_default_notify_me
: R.string.GroupMentionSettingDialog_default_dont_notify_me);
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);
}
public static class UpdatedBodyAndMentions {
@Nullable private final CharSequence body;
@NonNull private final List<Mention> mentions;
public UpdatedBodyAndMentions(@Nullable CharSequence body, @NonNull List<Mention> mentions) {
this.body = body;
this.mentions = mentions;
}
public @Nullable CharSequence getBody() {
return body;
}
public @NonNull List<Mention> getMentions() {
return mentions;
}
@Nullable String getBodyAsString() {
return body != null ? body.toString() : null;
}
}
}

View File

@@ -47,10 +47,12 @@ import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.documents.NetworkFailureList;
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Mention;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.NotificationMmsMessageRecord;
import org.thoughtcrime.securesms.database.model.Quote;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.jobs.TrimThreadJob;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
@@ -68,6 +70,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.revealable.ViewOnceExpirationInfo;
import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
import org.thoughtcrime.securesms.util.CursorUtil;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
@@ -109,9 +112,11 @@ public class MmsDatabase extends MessagingDatabase {
static final String QUOTE_BODY = "quote_body";
static final String QUOTE_ATTACHMENT = "quote_attachment";
static final String QUOTE_MISSING = "quote_missing";
static final String QUOTE_MENTIONS = "quote_mentions";
static final String SHARED_CONTACTS = "shared_contacts";
static final String LINK_PREVIEWS = "previews";
static final String MENTIONS_SELF = "mentions_self";
public static final String VIEW_ONCE = "reveal_duration";
@@ -163,6 +168,7 @@ public class MmsDatabase extends MessagingDatabase {
QUOTE_BODY + " TEXT, " +
QUOTE_ATTACHMENT + " INTEGER DEFAULT -1, " +
QUOTE_MISSING + " INTEGER DEFAULT 0, " +
QUOTE_MENTIONS + " BLOB DEFAULT NULL," +
SHARED_CONTACTS + " TEXT, " +
UNIDENTIFIED + " INTEGER DEFAULT 0, " +
LINK_PREVIEWS + " TEXT, " +
@@ -170,7 +176,8 @@ public class MmsDatabase extends MessagingDatabase {
REACTIONS + " BLOB DEFAULT NULL, " +
REACTIONS_UNREAD + " INTEGER DEFAULT 0, " +
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " +
REMOTE_DELETED + " INTEGER DEFAULT 0);";
REMOTE_DELETED + " INTEGER DEFAULT 0, " +
MENTIONS_SELF + " INTEGER DEFAULT 0);";
public static final String[] CREATE_INDEXS = {
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
@@ -193,9 +200,9 @@ public class MmsDatabase extends MessagingDatabase {
MESSAGE_SIZE, STATUS, TRANSACTION_ID,
BODY, PART_COUNT, RECIPIENT_ID, ADDRESS_DEVICE_ID,
DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID,
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_MISSING,
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_MISSING, QUOTE_MENTIONS,
SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN,
REMOTE_DELETED,
REMOTE_DELETED, MENTIONS_SELF,
"json_group_array(json_object(" +
"'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
"'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " +
@@ -805,6 +812,7 @@ public class MmsDatabase extends MessagingDatabase {
throws MmsException, NoSuchMessageException
{
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
MentionDatabase mentionDatabase = DatabaseFactory.getMentionDatabase(context);
Cursor cursor = null;
try {
@@ -812,6 +820,7 @@ public class MmsDatabase extends MessagingDatabase {
if (cursor != null && cursor.moveToNext()) {
List<DatabaseAttachment> associatedAttachments = attachmentDatabase.getAttachmentsForMessage(messageId);
List<Mention> mentions = mentionDatabase.getMentionsForMessage(messageId);
long outboxType = cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_BOX));
String body = cursor.getString(cursor.getColumnIndexOrThrow(BODY));
@@ -830,6 +839,7 @@ public class MmsDatabase extends MessagingDatabase {
String quoteText = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_BODY));
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(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);
@@ -846,7 +856,7 @@ public class MmsDatabase extends MessagingDatabase {
QuoteModel quote = null;
if (quoteId > 0 && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || !quoteAttachments.isEmpty())) {
quote = new QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteAttachments);
quote = new QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteAttachments, quoteMentions);
}
if (!TextUtils.isEmpty(mismatchDocument)) {
@@ -866,12 +876,12 @@ public class MmsDatabase extends MessagingDatabase {
}
if (body != null && (Types.isGroupQuit(outboxType) || Types.isGroupUpdate(outboxType))) {
return new OutgoingGroupUpdateMessage(recipient, new MessageGroupContext(body, Types.isGroupV2(outboxType)), attachments, timestamp, 0, false, quote, contacts, previews);
return new OutgoingGroupUpdateMessage(recipient, new MessageGroupContext(body, Types.isGroupV2(outboxType)), attachments, timestamp, 0, false, quote, contacts, previews, mentions);
} else if (Types.isExpirationTimerUpdate(outboxType)) {
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
}
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, viewOnce, distributionType, quote, contacts, previews, networkFailures, mismatches);
OutgoingMediaMessage message = new OutgoingMediaMessage(recipient, body, attachments, timestamp, subscriptionId, expiresIn, viewOnce, distributionType, quote, contacts, previews, mentions, networkFailures, mismatches);
if (Types.isSecureType(outboxType)) {
return new OutgoingSecureMediaMessage(message);
@@ -1000,10 +1010,15 @@ public class MmsDatabase extends MessagingDatabase {
if (retrieved.getQuote() != null) {
contentValues.put(QUOTE_ID, retrieved.getQuote().getId());
contentValues.put(QUOTE_BODY, retrieved.getQuote().getText());
contentValues.put(QUOTE_BODY, retrieved.getQuote().getText().toString());
contentValues.put(QUOTE_AUTHOR, retrieved.getQuote().getAuthor().serialize());
contentValues.put(QUOTE_MISSING, retrieved.getQuote().isOriginalMissing() ? 1 : 0);
BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(retrieved.getQuote().getMentions());
if (mentionsList != null) {
contentValues.put(QUOTE_MENTIONS, mentionsList.toByteArray());
}
quoteAttachments = retrieved.getQuote().getAttachments();
}
@@ -1012,7 +1027,7 @@ public class MmsDatabase extends MessagingDatabase {
return Optional.absent();
}
long messageId = insertMediaMessage(retrieved.getBody(), retrieved.getAttachments(), quoteAttachments, retrieved.getSharedContacts(), retrieved.getLinkPreviews(), contentValues, null);
long messageId = insertMediaMessage(threadId, retrieved.getBody(), retrieved.getAttachments(), quoteAttachments, retrieved.getSharedContacts(), retrieved.getLinkPreviews(), retrieved.getMentions(), contentValues, null);
if (!Types.isExpirationTimerUpdate(mailbox)) {
DatabaseFactory.getThreadDatabase(context).incrementUnread(threadId, 1);
@@ -1158,15 +1173,23 @@ public class MmsDatabase extends MessagingDatabase {
List<Attachment> quoteAttachments = new LinkedList<>();
if (message.getOutgoingQuote() != null) {
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithPlaceholders(message.getOutgoingQuote().getText(), message.getOutgoingQuote().getMentions());
contentValues.put(QUOTE_ID, message.getOutgoingQuote().getId());
contentValues.put(QUOTE_AUTHOR, message.getOutgoingQuote().getAuthor().serialize());
contentValues.put(QUOTE_BODY, message.getOutgoingQuote().getText());
contentValues.put(QUOTE_BODY, updated.getBodyAsString());
contentValues.put(QUOTE_MISSING, message.getOutgoingQuote().isOriginalMissing() ? 1 : 0);
BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(updated.getMentions());
if (mentionsList != null) {
contentValues.put(QUOTE_MENTIONS, mentionsList.toByteArray());
}
quoteAttachments.addAll(message.getOutgoingQuote().getAttachments());
}
long messageId = insertMediaMessage(message.getBody(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), contentValues, insertListener);
MentionUtil.UpdatedBodyAndMentions updatedBodyAndMentions = MentionUtil.updateBodyAndMentionsWithPlaceholders(message.getBody(), message.getMentions());
long messageId = insertMediaMessage(threadId, updatedBodyAndMentions.getBodyAsString(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), updatedBodyAndMentions.getMentions(), contentValues, insertListener);
if (message.getRecipient().isGroup()) {
OutgoingGroupUpdateMessage outgoingGroupUpdateMessage = (message instanceof OutgoingGroupUpdateMessage) ? (OutgoingGroupUpdateMessage) message : null;
@@ -1197,17 +1220,22 @@ public class MmsDatabase extends MessagingDatabase {
return messageId;
}
private long insertMediaMessage(@Nullable String body,
private long insertMediaMessage(long threadId,
@Nullable String body,
@NonNull List<Attachment> attachments,
@NonNull List<Attachment> quoteAttachments,
@NonNull List<Contact> sharedContacts,
@NonNull List<LinkPreview> linkPreviews,
@NonNull List<Mention> mentions,
@NonNull ContentValues contentValues,
@Nullable SmsDatabase.InsertListener insertListener)
throws MmsException
{
SQLiteDatabase db = databaseHelper.getWritableDatabase();
AttachmentDatabase partsDatabase = DatabaseFactory.getAttachmentDatabase(context);
SQLiteDatabase db = databaseHelper.getWritableDatabase();
AttachmentDatabase partsDatabase = DatabaseFactory.getAttachmentDatabase(context);
MentionDatabase mentionDatabase = DatabaseFactory.getMentionDatabase(context);
boolean mentionsSelf = Stream.of(mentions).filter(m -> Recipient.resolved(m.getRecipientId()).isLocalNumber()).findFirst().isPresent();
List<Attachment> allAttachments = new LinkedList<>();
List<Attachment> contactAttachments = Stream.of(sharedContacts).map(Contact::getAvatarAttachment).filter(a -> a != null).toList();
@@ -1219,11 +1247,14 @@ public class MmsDatabase extends MessagingDatabase {
contentValues.put(BODY, body);
contentValues.put(PART_COUNT, allAttachments.size());
contentValues.put(MENTIONS_SELF, mentionsSelf ? 1 : 0);
db.beginTransaction();
try {
long messageId = db.insert(TABLE_NAME, null, contentValues);
mentionDatabase.insert(threadId, messageId, mentions);
Map<Attachment, AttachmentId> insertedAttachments = partsDatabase.insertAttachmentsForMessage(messageId, allAttachments, quoteAttachments);
String serializedContacts = getSerializedSharedContacts(insertedAttachments, sharedContacts);
String serializedPreviews = getSerializedLinkPreviews(insertedAttachments, linkPreviews);
@@ -1468,6 +1499,12 @@ public class MmsDatabase extends MessagingDatabase {
}
}
private @NonNull List<Mention> parseQuoteMentions(Cursor cursor) {
byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(QUOTE_MENTIONS));
return MentionUtil.bodyRangeListToMentions(context, raw);
}
public void beginTransaction() {
databaseHelper.getWritableDatabase().beginTransaction();
}
@@ -1542,6 +1579,16 @@ public class MmsDatabase extends MessagingDatabase {
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();
if (quoteText != null && !quoteMentions.isEmpty()) {
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions);
quoteText = updated.getBody();
quoteMentions = updated.getMentions();
}
return new MediaMmsMessageRecord(id,
message.getRecipient(),
message.getRecipient(),
@@ -1564,14 +1611,16 @@ public class MmsDatabase extends MessagingDatabase {
message.getOutgoingQuote() != null ?
new Quote(message.getOutgoingQuote().getId(),
message.getOutgoingQuote().getAuthor(),
message.getOutgoingQuote().getText(),
quoteText,
message.getOutgoingQuote().isOriginalMissing(),
new SlideDeck(context, message.getOutgoingQuote().getAttachments())) :
new SlideDeck(context, message.getOutgoingQuote().getAttachments()),
quoteMentions) :
null,
message.getSharedContacts(),
message.getLinkPreviews(),
false,
Collections.emptyList(),
false,
false);
}
}
@@ -1665,6 +1714,7 @@ public class MmsDatabase extends MessagingDatabase {
boolean isViewOnce = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) == 1;
boolean remoteDelete = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.REMOTE_DELETED)) == 1;
List<ReactionRecord> reactions = parseReactions(cursor);
boolean mentionsSelf = CursorUtil.requireBoolean(cursor, MENTIONS_SELF);
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
readReceiptCount = 0;
@@ -1686,7 +1736,7 @@ public class MmsDatabase extends MessagingDatabase {
threadId, body, slideDeck, partCount, box, mismatches,
networkFailures, subscriptionId, expiresIn, expireStarted,
isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, reactions,
remoteDelete);
remoteDelete, mentionsSelf);
}
private List<IdentityKeyMismatch> getMismatchedIdentities(String document) {
@@ -1724,14 +1774,22 @@ public class MmsDatabase extends MessagingDatabase {
private @Nullable Quote getQuote(@NonNull Cursor cursor) {
long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_ID));
long quoteAuthor = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_AUTHOR));
String quoteText = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_BODY));
CharSequence quoteText = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_BODY));
boolean quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_MISSING)) == 1;
List<Mention> quoteMentions = parseQuoteMentions(cursor);
List<DatabaseAttachment> attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachment(cursor);
List<? extends Attachment> quoteAttachments = Stream.of(attachments).filter(Attachment::isQuote).toList();
SlideDeck quoteDeck = new SlideDeck(context, quoteAttachments);
if (quoteId > 0 && quoteAuthor > 0) {
return new Quote(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteDeck);
if (quoteText != null && !quoteMentions.isEmpty()) {
MentionUtil.UpdatedBodyAndMentions updated = MentionUtil.updateBodyAndMentionsWithDisplayNames(context, quoteText, quoteMentions);
quoteText = updated.getBody();
quoteMentions = updated.getMentions();
}
return new Quote(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteDeck, quoteMentions);
} else {
return null;
}

View File

@@ -82,6 +82,7 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.QUOTE_BODY,
MmsDatabase.QUOTE_MISSING,
MmsDatabase.QUOTE_ATTACHMENT,
MmsDatabase.QUOTE_MENTIONS,
MmsDatabase.SHARED_CONTACTS,
MmsDatabase.LINK_PREVIEWS,
MmsDatabase.VIEW_ONCE,
@@ -89,7 +90,8 @@ public class MmsSmsDatabase extends Database {
MmsSmsColumns.REACTIONS,
MmsSmsColumns.REACTIONS_UNREAD,
MmsSmsColumns.REACTIONS_LAST_SEEN,
MmsSmsColumns.REMOTE_DELETED};
MmsSmsColumns.REMOTE_DELETED,
MmsDatabase.MENTIONS_SELF};
public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
super(context, databaseHelper);
@@ -408,6 +410,7 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.QUOTE_BODY,
MmsDatabase.QUOTE_MISSING,
MmsDatabase.QUOTE_ATTACHMENT,
MmsDatabase.QUOTE_MENTIONS,
MmsDatabase.SHARED_CONTACTS,
MmsDatabase.LINK_PREVIEWS,
MmsDatabase.VIEW_ONCE,
@@ -415,7 +418,8 @@ public class MmsSmsDatabase extends Database {
MmsSmsColumns.REACTIONS_UNREAD,
MmsSmsColumns.REACTIONS_LAST_SEEN,
MmsSmsColumns.DATE_SERVER,
MmsSmsColumns.REMOTE_DELETED };
MmsSmsColumns.REMOTE_DELETED,
MmsDatabase.MENTIONS_SELF };
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
@@ -440,6 +444,7 @@ public class MmsSmsDatabase extends Database {
MmsDatabase.QUOTE_BODY,
MmsDatabase.QUOTE_MISSING,
MmsDatabase.QUOTE_ATTACHMENT,
MmsDatabase.QUOTE_MENTIONS,
MmsDatabase.SHARED_CONTACTS,
MmsDatabase.LINK_PREVIEWS,
MmsDatabase.VIEW_ONCE,
@@ -447,7 +452,8 @@ public class MmsSmsDatabase extends Database {
MmsSmsColumns.REACTIONS_UNREAD,
MmsSmsColumns.REACTIONS_LAST_SEEN,
MmsSmsColumns.DATE_SERVER,
MmsSmsColumns.REMOTE_DELETED };
MmsSmsColumns.REMOTE_DELETED,
MmsDatabase.MENTIONS_SELF };
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
@@ -493,6 +499,7 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsDatabase.QUOTE_BODY);
mmsColumnsPresent.add(MmsDatabase.QUOTE_MISSING);
mmsColumnsPresent.add(MmsDatabase.QUOTE_ATTACHMENT);
mmsColumnsPresent.add(MmsDatabase.QUOTE_MENTIONS);
mmsColumnsPresent.add(MmsDatabase.SHARED_CONTACTS);
mmsColumnsPresent.add(MmsDatabase.LINK_PREVIEWS);
mmsColumnsPresent.add(MmsDatabase.VIEW_ONCE);
@@ -500,6 +507,7 @@ public class MmsSmsDatabase extends Database {
mmsColumnsPresent.add(MmsDatabase.REACTIONS_UNREAD);
mmsColumnsPresent.add(MmsDatabase.REACTIONS_LAST_SEEN);
mmsColumnsPresent.add(MmsDatabase.REMOTE_DELETED);
mmsColumnsPresent.add(MmsDatabase.MENTIONS_SELF);
Set<String> smsColumnsPresent = new HashSet<>();
smsColumnsPresent.add(MmsSmsColumns.ID);

View File

@@ -119,13 +119,13 @@ public class RecipientDatabase extends Database {
private static final String PROFILE_GIVEN_NAME = "signal_profile_name";
private static final String PROFILE_FAMILY_NAME = "profile_family_name";
private static final String PROFILE_JOINED_NAME = "profile_joined_name";
private static final String MENTION_SETTING = "mention_setting";
public static final String SEARCH_PROFILE_NAME = "search_signal_profile";
private static final String SORT_NAME = "sort_name";
private static final String IDENTITY_STATUS = "identity_status";
private static final String IDENTITY_KEY = "identity_key";
private static final String[] RECIPIENT_PROJECTION = new String[] {
UUID, USERNAME, PHONE, EMAIL, GROUP_ID, GROUP_TYPE,
BLOCKED, MESSAGE_RINGTONE, CALL_RINGTONE, MESSAGE_VIBRATE, CALL_VIBRATE, MUTE_UNTIL, COLOR, SEEN_INVITE_REMINDER, DEFAULT_SUBSCRIPTION_ID, MESSAGE_EXPIRATION_TIME, REGISTERED,
@@ -136,7 +136,8 @@ public class RecipientDatabase extends Database {
UNIDENTIFIED_ACCESS_MODE,
FORCE_SMS_SELECTION,
UUID_CAPABILITY, GROUPS_V2_CAPABILITY,
STORAGE_SERVICE_ID, DIRTY
STORAGE_SERVICE_ID, DIRTY,
MENTION_SETTING
};
private static final String[] ID_PROJECTION = new String[]{ID};
@@ -146,6 +147,8 @@ public class RecipientDatabase extends Database {
.map(columnName -> TABLE_NAME + "." + columnName)
.toList().toArray(new String[0]);
private static final String[] MENTION_SEARCH_PROJECTION = new String[]{ID, removeWhitespace("COALESCE(" + nullIfEmpty(SYSTEM_DISPLAY_NAME) + ", " + nullIfEmpty(PROFILE_JOINED_NAME) + ", " + nullIfEmpty(PROFILE_GIVEN_NAME) + ", " + nullIfEmpty(USERNAME) + ", " + nullIfEmpty(PHONE) + ")") + " AS " + SORT_NAME};
private static final String[] RECIPIENT_FULL_PROJECTION = ArrayUtils.concat(
new String[] { TABLE_NAME + "." + ID },
TYPED_RECIPIENT_PROJECTION,
@@ -275,6 +278,24 @@ public class RecipientDatabase extends Database {
}
}
public enum MentionSetting {
GLOBAL(0), ALWAYS_NOTIFY(1), DO_NOT_NOTIFY(2);
private final int id;
MentionSetting(int id) {
this.id = id;
}
int getId() {
return id;
}
public static MentionSetting fromId(int id) {
return values()[id];
}
}
public static final String CREATE_TABLE =
"CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
UUID + " TEXT UNIQUE DEFAULT NULL, " +
@@ -314,7 +335,8 @@ public class RecipientDatabase extends Database {
UUID_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " +
GROUPS_V2_CAPABILITY + " INTEGER DEFAULT " + Recipient.Capability.UNKNOWN.serialize() + ", " +
STORAGE_SERVICE_ID + " TEXT UNIQUE DEFAULT NULL, " +
DIRTY + " INTEGER DEFAULT " + DirtyState.CLEAN.getId() + ");";
DIRTY + " INTEGER DEFAULT " + DirtyState.CLEAN.getId() + ", " +
MENTION_SETTING + " INTEGER DEFAULT " + MentionSetting.GLOBAL.getId() + ");";
private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID +
" FROM " + TABLE_NAME +
@@ -1078,6 +1100,7 @@ public class RecipientDatabase extends Database {
int uuidCapabilityValue = CursorUtil.requireInt(cursor, UUID_CAPABILITY);
int groupsV2CapabilityValue = CursorUtil.requireInt(cursor, GROUPS_V2_CAPABILITY);
String storageKeyRaw = CursorUtil.requireString(cursor, STORAGE_SERVICE_ID);
int mentionSettingId = CursorUtil.requireInt(cursor, MENTION_SETTING);
Optional<String> identityKeyRaw = CursorUtil.getString(cursor, IDENTITY_KEY);
Optional<Integer> identityStatusRaw = CursorUtil.getInt(cursor, IDENTITY_STATUS);
@@ -1145,7 +1168,7 @@ public class RecipientDatabase extends Database {
Recipient.Capability.deserialize(uuidCapabilityValue),
Recipient.Capability.deserialize(groupsV2CapabilityValue),
InsightsBannerTier.fromId(insightsBannerTier),
storageKey, identityKey, identityStatus);
storageKey, identityKey, identityStatus, MentionSetting.fromId(mentionSettingId));
}
public BulkOperationsHandle beginBulkSystemContactUpdate() {
@@ -1299,6 +1322,14 @@ public class RecipientDatabase extends Database {
}
}
public void setMentionSetting(@NonNull RecipientId id, @NonNull MentionSetting mentionSetting) {
ContentValues values = new ContentValues();
values.put(MENTION_SETTING, mentionSetting.getId());
if (update(id, values)) {
Recipient.live(id).refresh();
}
}
/**
* Updates the profile key.
* <p>
@@ -1902,6 +1933,29 @@ public class RecipientDatabase extends Database {
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null);
}
public @NonNull List<Recipient> queryRecipientsForMentions(@NonNull String query, @NonNull List<RecipientId> recipientIds) {
if (TextUtils.isEmpty(query) || recipientIds.isEmpty()) {
return Collections.emptyList();
}
query = buildCaseInsensitiveGlobPattern(query);
String ids = TextUtils.join(",", Stream.of(recipientIds).map(RecipientId::serialize).toList());
String selection = BLOCKED + " = 0 AND " +
ID + " IN (" + ids + ") AND " +
SORT_NAME + " GLOB ?";
List<Recipient> recipients = new ArrayList<>();
try (RecipientDatabase.RecipientReader reader = new RecipientReader(databaseHelper.getReadableDatabase().query(TABLE_NAME, MENTION_SEARCH_PROJECTION, selection, SqlUtil.buildArgs(query), null, null, SORT_NAME))) {
Recipient recipient;
while ((recipient = reader.getNext()) != null) {
recipients.add(recipient);
}
}
return recipients;
}
/**
* Builds a case-insensitive GLOB pattern for fuzzy text queries. Works with all unicode
* characters.
@@ -2384,6 +2438,10 @@ public class RecipientDatabase extends Database {
return "NULLIF(" + column + ", '')";
}
private static @NonNull String removeWhitespace(@NonNull String column) {
return "REPLACE(" + column + ", ' ', '')";
}
public interface ColorUpdater {
MaterialColor update(@NonNull String name, @Nullable String color);
}
@@ -2427,6 +2485,7 @@ public class RecipientDatabase extends Database {
private final byte[] storageId;
private final byte[] identityKey;
private final IdentityDatabase.VerifiedStatus identityStatus;
private final MentionSetting mentionSetting;
RecipientSettings(@NonNull RecipientId id,
@Nullable UUID uuid,
@@ -2465,7 +2524,8 @@ public class RecipientDatabase extends Database {
@NonNull InsightsBannerTier insightsBannerTier,
@Nullable byte[] storageId,
@Nullable byte[] identityKey,
@NonNull IdentityDatabase.VerifiedStatus identityStatus)
@NonNull IdentityDatabase.VerifiedStatus identityStatus,
@NonNull MentionSetting mentionSetting)
{
this.id = id;
this.uuid = uuid;
@@ -2505,6 +2565,7 @@ public class RecipientDatabase extends Database {
this.storageId = storageId;
this.identityKey = identityKey;
this.identityStatus = identityStatus;
this.mentionSetting = mentionSetting;
}
public RecipientId getId() {
@@ -2661,6 +2722,10 @@ public class RecipientDatabase extends Database {
public @NonNull IdentityDatabase.VerifiedStatus getIdentityStatus() {
return identityStatus;
}
public @NonNull MentionSetting getMentionSetting() {
return mentionSetting;
}
}
public static class RecipientReader implements Closeable {

View File

@@ -66,7 +66,7 @@ public final class ThreadBodyUtil {
return context.getString(R.string.ThreadRecord_media_message);
} else {
Log.w(TAG, "Got a media message with a body of a type we were not able to process. [contains media slide]:" + record.containsMediaSlide());
return record.getBody();
return getBody(context, record);
}
}
@@ -75,10 +75,10 @@ public final class ThreadBodyUtil {
}
private static @NonNull String getBodyOrDefault(@NonNull Context context, @NonNull MessageRecord record, @StringRes int defaultStringRes) {
if (TextUtils.isEmpty(record.getBody())) {
return context.getString(defaultStringRes);
} else {
return record.getBody();
}
return TextUtils.isEmpty(record.getBody()) ? context.getString(defaultStringRes) : getBody(context, record);
}
private static @NonNull String getBody(@NonNull Context context, @NonNull MessageRecord record) {
return MentionUtil.updateBodyWithDisplayNames(context, record, record.getBody()).toString();
}
}

View File

@@ -22,6 +22,7 @@ import net.sqlcipher.database.SQLiteDatabaseHook;
import net.sqlcipher.database.SQLiteOpenHelper;
import org.thoughtcrime.securesms.contacts.avatars.ContactColorsLegacy;
import org.thoughtcrime.securesms.database.MentionDatabase;
import org.thoughtcrime.securesms.database.RemappedRecordsDatabase;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
@@ -139,8 +140,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int QUOTE_CLEANUP = 65;
private static final int BORDERLESS = 66;
private static final int REMAPPED_RECORDS = 67;
private static final int MENTIONS = 68;
private static final int DATABASE_VERSION = 67;
private static final int DATABASE_VERSION = 68;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -184,6 +186,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
db.execSQL(StorageKeyDatabase.CREATE_TABLE);
db.execSQL(KeyValueDatabase.CREATE_TABLE);
db.execSQL(MegaphoneDatabase.CREATE_TABLE);
db.execSQL(MentionDatabase.CREATE_TABLE);
executeStatements(db, SearchDatabase.CREATE_TABLE);
executeStatements(db, JobDatabase.CREATE_TABLE);
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE);
@@ -198,6 +201,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES);
executeStatements(db, StickerDatabase.CREATE_INDEXES);
executeStatements(db, StorageKeyDatabase.CREATE_INDEXES);
executeStatements(db, MentionDatabase.CREATE_INDEXES);
if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) {
ClassicOpenHelper legacyHelper = new ClassicOpenHelper(context);
@@ -970,6 +974,23 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
"new_id INTEGER)");
}
if (oldVersion < MENTIONS) {
db.execSQL("CREATE TABLE mention (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
"thread_id INTEGER, " +
"message_id INTEGER, " +
"recipient_id INTEGER, " +
"range_start INTEGER, " +
"range_length INTEGER)");
db.execSQL("CREATE INDEX IF NOT EXISTS mention_message_id_index ON mention (message_id)");
db.execSQL("CREATE INDEX IF NOT EXISTS mention_recipient_id_thread_id_index ON mention (recipient_id, thread_id);");
db.execSQL("ALTER TABLE mms ADD COLUMN quote_mentions BLOB DEFAULT NULL");
db.execSQL("ALTER TABLE mms ADD COLUMN mentions_self INTEGER DEFAULT 0");
db.execSQL("ALTER TABLE recipient ADD COLUMN mention_setting INTEGER DEFAULT 0");
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();

View File

@@ -45,6 +45,7 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
private final static String TAG = MediaMmsMessageRecord.class.getSimpleName();
private final int partCount;
private final boolean mentionsSelf;
public MediaMmsMessageRecord(long id,
Recipient conversationRecipient,
@@ -71,19 +72,26 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
@NonNull List<LinkPreview> linkPreviews,
boolean unidentified,
@NonNull List<ReactionRecord> reactions,
boolean remoteDelete)
boolean remoteDelete,
boolean mentionsSelf)
{
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent,
dateReceived, dateServer, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
subscriptionId, expiresIn, expireStarted, viewOnce, slideDeck,
readReceiptCount, quote, contacts, linkPreviews, unidentified, reactions, remoteDelete);
this.partCount = partCount;
this.partCount = partCount;
this.mentionsSelf = mentionsSelf;
}
public int getPartCount() {
return partCount;
}
@Override
public boolean hasSelfMention() {
return mentionsSelf;
}
@Override
public boolean isMmsNotification() {
return false;

View File

@@ -0,0 +1,90 @@
package org.thoughtcrime.securesms.database.model;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.Objects;
public class Mention implements Comparable<Mention>, Parcelable {
private final RecipientId recipientId;
private final int start;
private final int length;
public Mention(@NonNull RecipientId recipientId, int start, int length) {
this.recipientId = recipientId;
this.start = start;
this.length = length;
}
protected Mention(Parcel in) {
recipientId = in.readParcelable(RecipientId.class.getClassLoader());
start = in.readInt();
length = in.readInt();
}
public @NonNull RecipientId getRecipientId() {
return recipientId;
}
public int getStart() {
return start;
}
public int getLength() {
return length;
}
@Override
public int compareTo(Mention other) {
return Integer.compare(start, other.start);
}
@Override
public int hashCode() {
return Objects.hash(recipientId, start, length);
}
@Override
public boolean equals(@Nullable Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
Mention that = (Mention) object;
return recipientId.equals(that.recipientId) && start == that.start && length == that.length;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeParcelable(recipientId, flags);
dest.writeInt(start);
dest.writeInt(length);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<Mention> CREATOR = new Creator<Mention>() {
@Override
public Mention createFromParcel(Parcel in) {
return new Mention(in);
}
@Override
public Mention[] newArray(int size) {
return new Mention[size];
}
};
}

View File

@@ -374,4 +374,8 @@ public abstract class MessageRecord extends DisplayRecord {
public @NonNull List<ReactionRecord> getReactions() {
return reactions;
}
public boolean hasSelfMention() {
return false;
}
}

View File

@@ -1,26 +1,42 @@
package org.thoughtcrime.securesms.database.model;
import android.text.SpannableString;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.List;
public class Quote {
private final long id;
private final RecipientId author;
private final String text;
private final boolean missing;
private final SlideDeck attachment;
private final long id;
private final RecipientId author;
private final CharSequence text;
private final boolean missing;
private final SlideDeck attachment;
private final List<Mention> mentions;
public Quote(long id, @NonNull RecipientId author, @Nullable String text, boolean missing, @NonNull SlideDeck attachment) {
this.id = id;
this.author = author;
this.text = text;
this.missing = missing;
this.attachment = attachment;
public Quote(long id,
@NonNull RecipientId author,
@Nullable CharSequence text,
boolean missing,
@NonNull SlideDeck attachment,
@NonNull List<Mention> mentions)
{
this.id = id;
this.author = author;
this.missing = missing;
this.attachment = attachment;
this.mentions = mentions;
SpannableString spannable = new SpannableString(text);
MentionAnnotation.setMentionAnnotations(spannable, mentions);
this.text = spannable;
}
public long getId() {
@@ -31,7 +47,7 @@ public class Quote {
return author;
}
public @Nullable String getText() {
public @Nullable CharSequence getDisplayText() {
return text;
}