From ab55fec6bd42060e9cf2cb6e10c2dd59c611ca43 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 11 Nov 2021 13:12:51 -0500 Subject: [PATCH] Move reactions into their own table. --- .../database/RecipientDatabaseTest_merges.kt | 17 ++ .../conversation/ConversationActivity.java | 8 +- .../conversation/ConversationDataSource.java | 58 ++++- .../securesms/database/DatabaseFactory.java | 6 + .../securesms/database/MessageDatabase.java | 151 ++----------- .../securesms/database/MmsDatabase.java | 7 +- .../securesms/database/MmsSmsColumns.java | 1 - .../securesms/database/MmsSmsDatabase.java | 6 - .../securesms/database/ReactionDatabase.kt | 199 ++++++++++++++++++ .../securesms/database/RecipientDatabase.java | 77 +++---- .../securesms/database/SmsDatabase.java | 7 +- .../database/helpers/SQLCipherOpenHelper.java | 72 ++++++- .../database/model/MediaMmsMessageRecord.java | 7 + .../database/model/ReactionRecord.java | 57 ----- .../database/model/ReactionRecord.kt | 13 ++ .../database/model/SmsMessageRecord.java | 7 + .../securesms/jobs/ReactionSendJob.java | 51 ++--- .../messages/MessageContentProcessor.java | 6 +- .../migrations/ApplicationMigrations.java | 7 +- .../v2/NotificationStateProvider.kt | 9 +- .../securesms/reactions/ReactionDetails.java | 35 --- .../securesms/reactions/ReactionDetails.kt | 13 ++ .../ReactionsBottomSheetDialogFragment.java | 38 ++-- .../securesms/reactions/ReactionsLoader.java | 110 ---------- .../reactions/ReactionsRepository.kt | 48 +++++ .../reactions/ReactionsViewModel.java | 59 +++--- ...WithAnyEmojiBottomSheetDialogFragment.java | 21 +- .../any/ReactWithAnyEmojiRepository.java | 27 +-- .../any/ReactWithAnyEmojiViewModel.java | 73 +++---- .../securesms/sms/MessageSender.java | 19 +- .../securesms/util/LifecycleDisposable.kt | 4 + app/src/main/proto/Database.proto | 4 +- 32 files changed, 650 insertions(+), 567 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/ReactionDatabase.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/model/ReactionRecord.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/model/ReactionRecord.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsLoader.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsRepository.kt diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_merges.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_merges.kt index a99f0da5b0..c3c7f53a29 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_merges.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/database/RecipientDatabaseTest_merges.kt @@ -16,7 +16,9 @@ import org.signal.storageservice.protos.groups.local.DecryptedGroup import org.signal.storageservice.protos.groups.local.DecryptedMember import org.signal.zkgroup.groups.GroupMasterKey import org.thoughtcrime.securesms.database.model.Mention +import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.database.model.ReactionRecord import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.mms.IncomingMediaMessage import org.thoughtcrime.securesms.recipients.Recipient @@ -43,6 +45,7 @@ class RecipientDatabaseTest_merges { private lateinit var mmsDatabase: MessageDatabase private lateinit var sessionDatabase: SessionDatabase private lateinit var mentionDatabase: MentionDatabase + private lateinit var reactionDatabase: ReactionDatabase @Before fun setup() { @@ -55,6 +58,7 @@ class RecipientDatabaseTest_merges { mmsDatabase = DatabaseFactory.getMmsDatabase(context) sessionDatabase = DatabaseFactory.getSessionDatabase(context) mentionDatabase = DatabaseFactory.getMentionDatabase(context) + reactionDatabase = DatabaseFactory.getReactionDatabase(context) ensureDbEmpty() } @@ -91,6 +95,9 @@ class RecipientDatabaseTest_merges { sessionDatabase.store(SignalProtocolAddress(ACI_A.toString(), 1), SessionRecord()) + reactionDatabase.addReaction(MessageId(smsId1, false), ReactionRecord("a", recipientIdAci, 1, 1)) + reactionDatabase.addReaction(MessageId(mmsId1, true), ReactionRecord("b", recipientIdE164, 1, 1)) + // Merge val retrievedId: RecipientId = recipientDatabase.getAndPossiblyMerge(ACI_A, E164_A, true) val retrievedThreadId: Long = threadDatabase.getThreadIdFor(retrievedId)!! @@ -155,6 +162,16 @@ class RecipientDatabaseTest_merges { // Session validation assertNotNull(sessionDatabase.load(SignalProtocolAddress(ACI_A.toString(), 1))) + + // Reaction validation + val reactionsSms: List = reactionDatabase.getReactions(MessageId(smsId1, false)) + val reactionsMms: List = reactionDatabase.getReactions(MessageId(mmsId1, true)) + + assertEquals(1, reactionsSms.size) + assertEquals(ReactionRecord("a", recipientIdAci, 1, 1), reactionsSms[0]) + + assertEquals(1, reactionsMms.size) + assertEquals(ReactionRecord("b", recipientIdAci, 1, 1), reactionsMms[0]) } private val context: Application diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index 40741fbee4..b2200ee8c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -167,6 +167,7 @@ import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.identity.IdentityRecordList; import org.thoughtcrime.securesms.database.model.IdentityRecord; import org.thoughtcrime.securesms.database.model.Mention; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.ReactionRecord; @@ -2361,9 +2362,9 @@ public class ConversationActivity extends PassphraseRequiredActivity .orElse(null); if (oldRecord != null && oldRecord.getEmoji().equals(emoji)) { - MessageSender.sendReactionRemoval(context, messageRecord.getId(), messageRecord.isMms(), oldRecord); + MessageSender.sendReactionRemoval(context, new MessageId(messageRecord.getId(), messageRecord.isMms()), oldRecord); } else { - MessageSender.sendNewReaction(context, messageRecord.getId(), messageRecord.isMms(), emoji); + MessageSender.sendNewReaction(context, new MessageId(messageRecord.getId(), messageRecord.isMms()), emoji); } }); } @@ -2381,8 +2382,7 @@ public class ConversationActivity extends PassphraseRequiredActivity reactionDelegate.hide(); SignalExecutors.BOUNDED.execute(() -> MessageSender.sendReactionRemoval(context, - messageRecord.getId(), - messageRecord.isMms(), + new MessageId(messageRecord.getId(), messageRecord.isMms()), oldRecord)); } else { reactionDelegate.hideForReactWithAny(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java index 961d6443a9..a612ab98a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java @@ -13,7 +13,6 @@ import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.conversation.ConversationData.MessageRequestData; import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory; import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MentionDatabase; import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.MmsSmsDatabase; import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord; @@ -21,6 +20,8 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.Mention; import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.database.model.ReactionRecord; +import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.util.Stopwatch; import org.thoughtcrime.securesms.util.Util; @@ -72,12 +73,14 @@ class ConversationDataSource implements PagedDataSource records = new ArrayList<>(length); MentionHelper mentionHelper = new MentionHelper(); AttachmentHelper attachmentHelper = new AttachmentHelper(); + ReactionHelper reactionHelper = new ReactionHelper(); try (MmsSmsDatabase.Reader reader = MmsSmsDatabase.readerFor(db.getConversation(threadId, start, length))) { MessageRecord record; while ((record = reader.getNext()) != null && !cancellationSignal.isCanceled()) { records.add(record); mentionHelper.add(record); + reactionHelper.add(record); attachmentHelper.add(record); } } @@ -93,15 +96,18 @@ class ConversationDataSource implements PagedDataSource messages = Stream.of(records) @@ -133,6 +139,11 @@ class ConversationDataSource implements PagedDataSource reactions = DatabaseFactory.getReactionDatabase(context).getReactions(messageId); + record = ReactionHelper.recordWithReactions(record, reactions); + + stopwatch.split("reactions"); + if (messageId.isMms()) { List attachments = DatabaseFactory.getAttachmentDatabase(context).getAttachmentsForMessage(messageId.getId()); if (attachments.size() > 0) { @@ -208,4 +219,43 @@ class ConversationDataSource implements PagedDataSource messageIds = new LinkedList<>(); + private Map> messageIdToReactions = new HashMap<>(); + + void add(MessageRecord record) { + messageIds.add(new MessageId(record.getId(), record.isMms())); + } + + void fetchReactions(Context context) { + messageIdToReactions = DatabaseFactory.getReactionDatabase(context).getReactionsForMessages(messageIds); + } + + @NonNull List buildUpdatedModels(@NonNull Context context, @NonNull List records) { + return records.stream() + .map(record -> { + MessageId messageId = new MessageId(record.getId(), record.isMms()); + List reactions = messageIdToReactions.get(messageId); + + return recordWithReactions(record, reactions); + }) + .collect(Collectors.toList()); + } + + static MessageRecord recordWithReactions(@NonNull MessageRecord record, List reactions) { + if (Util.hasItems(reactions)) { + if (record instanceof MediaMmsMessageRecord) { + return ((MediaMmsMessageRecord) record).withReactions(reactions); + } else if (record instanceof SmsMessageRecord) { + return ((SmsMessageRecord) record).withReactions(reactions); + } else { + throw new IllegalStateException("We have reactions for an unsupported record type: " + record.getClass().getName()); + } + } else { + return record; + } + } + } + } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java index 1599a9ae9d..beb857edbc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -76,6 +76,7 @@ public class DatabaseFactory { private final MessageSendLogDatabase messageSendLogDatabase; private final AvatarPickerDatabase avatarPickerDatabase; private final GroupCallRingDatabase groupCallRingDatabase; + private final ReactionDatabase reactionDatabase; public static DatabaseFactory getInstance(Context context) { if (instance == null) { @@ -214,6 +215,10 @@ public class DatabaseFactory { return getInstance(context).groupCallRingDatabase; } + public static ReactionDatabase getReactionDatabase(Context context) { + return getInstance(context).reactionDatabase; + } + public static net.zetetic.database.sqlcipher.SQLiteDatabase getBackupDatabase(Context context) { return getInstance(context).databaseHelper.getRawReadableDatabase(); } @@ -274,6 +279,7 @@ public class DatabaseFactory { this.messageSendLogDatabase = new MessageSendLogDatabase(context, databaseHelper); this.avatarPickerDatabase = new AvatarPickerDatabase(context, databaseHelper); this.groupCallRingDatabase = new GroupCallRingDatabase(context, databaseHelper); + this.reactionDatabase = new ReactionDatabase(context, databaseHelper); } public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java index e0d63fe910..1bdef942e0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -310,73 +310,6 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns db.update(getTableName(), values, query, args); } - public void addReaction(long messageId, @NonNull ReactionRecord reaction) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - - try { - ReactionList reactions = getReactions(db, messageId).or(ReactionList.getDefaultInstance()); - ReactionList.Reaction newReaction = ReactionList.Reaction.newBuilder() - .setEmoji(reaction.getEmoji()) - .setAuthor(reaction.getAuthor().toLong()) - .setSentTime(reaction.getDateSent()) - .setReceivedTime(reaction.getDateReceived()) - .build(); - - ReactionList updatedList = pruneByAuthor(reactions, reaction.getAuthor()).toBuilder() - .addReactions(newReaction) - .build(); - - setReactions(db, messageId, updatedList); - - db.setTransactionSuccessful(); - } catch (NoSuchMessageException e) { - Log.w(TAG, "No message for provided id", e); - } finally { - db.endTransaction(); - } - - notifyConversationListeners(getThreadId(db, messageId)); - } - - public void deleteReaction(long messageId, @NonNull RecipientId author) { - SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); - - db.beginTransaction(); - - try { - ReactionList reactions = getReactions(db, messageId).or(ReactionList.getDefaultInstance()); - ReactionList updatedList = pruneByAuthor(reactions, author); - - setReactions(db, messageId, updatedList); - - db.setTransactionSuccessful(); - } catch (NoSuchMessageException e) { - Log.w(TAG, "No message for provided id", e); - } finally { - db.endTransaction(); - } - - notifyConversationListeners(getThreadId(db, messageId)); - } - - public boolean hasReaction(long messageId, @NonNull ReactionRecord reactionRecord) { - SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - - ReactionList reactions = getReactions(db, messageId).or(ReactionList.getDefaultInstance()); - - for (ReactionList.Reaction reaction : reactions.getReactionsList()) { - if (reactionRecord.getAuthor().toLong() == reaction.getAuthor() && - reactionRecord.getEmoji().equals(reaction.getEmoji())) - { - return true; - } - } - - return false; - } - public void setNotifiedTimestamp(long timestamp, @NonNull List ids) { if (ids.isEmpty()) { return; @@ -438,25 +371,26 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns return data; } - protected static List parseReactions(@NonNull Cursor cursor) { - byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(REACTIONS)); + void updateReactionsUnread(SQLiteDatabase db, long messageId, boolean hasReactions, boolean isRemoval) { + try { + boolean isOutgoing = getMessageRecord(messageId).isOutgoing(); + ContentValues values = new ContentValues(); - if (raw != null) { - try { - return Stream.of(ReactionList.parseFrom(raw).getReactionsList()) - .map(r -> { - return new ReactionRecord(r.getEmoji(), - RecipientId.from(r.getAuthor()), - r.getSentTime(), - r.getReceivedTime()); - }) - .toList(); - } catch (InvalidProtocolBufferException e) { - Log.w(TAG, "[parseReactions] Failed to parse reaction list!", e); - return Collections.emptyList(); + if (!hasReactions) { + values.put(REACTIONS_UNREAD, 0); + } else if (!isRemoval) { + values.put(REACTIONS_UNREAD, 1); } - } else { - return Collections.emptyList(); + + if (isOutgoing && hasReactions) { + values.put(NOTIFIED, 0); + } + + if (values.size() > 0) { + db.update(getTableName(), values, ID_WHERE, SqlUtil.buildArgs(messageId)); + } + } catch (NoSuchMessageException e) { + Log.w(TAG, "Failed to find message " + messageId); } } @@ -555,55 +489,6 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns } } - private static @NonNull ReactionList pruneByAuthor(@NonNull ReactionList reactionList, @NonNull RecipientId recipientId) { - List pruned = Stream.of(reactionList.getReactionsList()) - .filterNot(r -> r.getAuthor() == recipientId.toLong()) - .toList(); - - return reactionList.toBuilder() - .clearReactions() - .addAllReactions(pruned) - .build(); - } - - private @NonNull Optional getReactions(SQLiteDatabase db, long messageId) { - String[] projection = new String[]{ REACTIONS }; - String query = ID + " = ?"; - String[] args = new String[]{String.valueOf(messageId)}; - - try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null)) { - if (cursor != null && cursor.moveToFirst()) { - byte[] raw = cursor.getBlob(cursor.getColumnIndexOrThrow(REACTIONS)); - - if (raw != null) { - return Optional.of(ReactionList.parseFrom(raw)); - } - } - } catch (InvalidProtocolBufferException e) { - Log.w(TAG, "[getRecipients] Failed to parse reaction list!", e); - } - - return Optional.absent(); - } - - private void setReactions(@NonNull SQLiteDatabase db, long messageId, @NonNull ReactionList reactionList) throws NoSuchMessageException { - ContentValues values = new ContentValues(); - boolean isOutgoing = getMessageRecord(messageId).isOutgoing(); - boolean hasReactions = reactionList.getReactionsCount() != 0; - - values.put(REACTIONS, reactionList.getReactionsList().isEmpty() ? null : reactionList.toByteArray()); - values.put(REACTIONS_UNREAD, hasReactions ? 1 : 0); - - if (isOutgoing && hasReactions) { - values.put(NOTIFIED, 0); - } - - String query = ID + " = ?"; - String[] args = new String[] { String.valueOf(messageId) }; - - db.update(getTableName(), values, query, args); - } - private long getThreadId(@NonNull SQLiteDatabase db, long messageId) { String[] projection = new String[]{ THREAD_ID }; String query = ID + " = ?"; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 5835cc4000..aabc7b1938 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -163,7 +163,6 @@ public class MmsDatabase extends MessageDatabase { UNIDENTIFIED + " INTEGER DEFAULT 0, " + LINK_PREVIEWS + " TEXT, " + VIEW_ONCE + " INTEGER DEFAULT 0, " + - REACTIONS + " BLOB DEFAULT NULL, " + REACTIONS_UNREAD + " INTEGER DEFAULT 0, " + REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " + REMOTE_DELETED + " INTEGER DEFAULT 0, " + @@ -193,7 +192,7 @@ public class MmsDatabase extends MessageDatabase { 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, QUOTE_MENTIONS, - SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN, + SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS_UNREAD, REACTIONS_LAST_SEEN, REMOTE_DELETED, MENTIONS_SELF, NOTIFIED_TIMESTAMP, VIEWED_RECEIPT_COUNT, RECEIPT_TIMESTAMP, "json_group_array(json_object(" + "'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " + @@ -855,7 +854,6 @@ public class MmsDatabase extends MessageDatabase { values.putNull(QUOTE_ID); values.putNull(LINK_PREVIEWS); values.putNull(SHARED_CONTACTS); - values.putNull(REACTIONS); db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(messageId) }); DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentsForMessage(messageId); @@ -2099,7 +2097,6 @@ public class MmsDatabase extends MessageDatabase { boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.UNIDENTIFIED)) == 1; boolean isViewOnce = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) == 1; boolean remoteDelete = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.REMOTE_DELETED)) == 1; - List reactions = parseReactions(cursor); boolean mentionsSelf = CursorUtil.requireBoolean(cursor, MENTIONS_SELF); long notifiedTimestamp = CursorUtil.requireLong(cursor, NOTIFIED_TIMESTAMP); int viewedReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.VIEWED_RECEIPT_COUNT)); @@ -2128,7 +2125,7 @@ public class MmsDatabase extends MessageDatabase { addressDeviceId, dateSent, dateReceived, dateServer, deliveryReceiptCount, threadId, body, slideDeck, partCount, box, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, - isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, reactions, + isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, Collections.emptyList(), remoteDelete, mentionsSelf, notifiedTimestamp, viewedReceiptCount, receiptTimestamp); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 6b76c73f59..b885053b97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -24,7 +24,6 @@ public interface MmsSmsColumns { public static final String NOTIFIED = "notified"; public static final String NOTIFIED_TIMESTAMP = "notified_timestamp"; public static final String UNIDENTIFIED = "unidentified"; - public static final String REACTIONS = "reactions"; public static final String REACTIONS_UNREAD = "reactions_unread"; public static final String REACTIONS_LAST_SEEN = "reactions_last_seen"; public static final String REMOTE_DELETED = "remote_deleted"; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index c549e2a008..62f7d80ccd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -74,7 +74,6 @@ public class MmsSmsDatabase extends Database { MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsSmsColumns.UNIDENTIFIED, - MmsSmsColumns.REACTIONS, MmsDatabase.PART_COUNT, MmsDatabase.CONTENT_LOCATION, MmsDatabase.TRANSACTION_ID, @@ -101,7 +100,6 @@ public class MmsSmsDatabase extends Database { MmsDatabase.LINK_PREVIEWS, MmsDatabase.VIEW_ONCE, MmsSmsColumns.READ, - MmsSmsColumns.REACTIONS, MmsSmsColumns.REACTIONS_UNREAD, MmsSmsColumns.REACTIONS_LAST_SEEN, MmsSmsColumns.REMOTE_DELETED, @@ -678,7 +676,6 @@ public class MmsSmsDatabase extends Database { MmsDatabase.SHARED_CONTACTS, MmsDatabase.LINK_PREVIEWS, MmsDatabase.VIEW_ONCE, - MmsDatabase.REACTIONS, MmsSmsColumns.REACTIONS_UNREAD, MmsSmsColumns.REACTIONS_LAST_SEEN, MmsSmsColumns.DATE_SERVER, @@ -712,7 +709,6 @@ public class MmsSmsDatabase extends Database { MmsDatabase.SHARED_CONTACTS, MmsDatabase.LINK_PREVIEWS, MmsDatabase.VIEW_ONCE, - MmsDatabase.REACTIONS, MmsSmsColumns.REACTIONS_UNREAD, MmsSmsColumns.REACTIONS_LAST_SEEN, MmsSmsColumns.DATE_SERVER, @@ -775,7 +771,6 @@ public class MmsSmsDatabase extends Database { mmsColumnsPresent.add(MmsDatabase.SHARED_CONTACTS); mmsColumnsPresent.add(MmsDatabase.LINK_PREVIEWS); mmsColumnsPresent.add(MmsDatabase.VIEW_ONCE); - mmsColumnsPresent.add(MmsDatabase.REACTIONS); mmsColumnsPresent.add(MmsDatabase.REACTIONS_UNREAD); mmsColumnsPresent.add(MmsDatabase.REACTIONS_LAST_SEEN); mmsColumnsPresent.add(MmsDatabase.REMOTE_DELETED); @@ -805,7 +800,6 @@ public class MmsSmsDatabase extends Database { smsColumnsPresent.add(SmsDatabase.DATE_SERVER); smsColumnsPresent.add(SmsDatabase.STATUS); smsColumnsPresent.add(SmsDatabase.UNIDENTIFIED); - smsColumnsPresent.add(SmsDatabase.REACTIONS); smsColumnsPresent.add(SmsDatabase.REACTIONS_UNREAD); smsColumnsPresent.add(SmsDatabase.REACTIONS_LAST_SEEN); smsColumnsPresent.add(MmsDatabase.REMOTE_DELETED); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ReactionDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ReactionDatabase.kt new file mode 100644 index 0000000000..75ce45a13b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ReactionDatabase.kt @@ -0,0 +1,199 @@ +package org.thoughtcrime.securesms.database + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.database.model.ReactionRecord +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.util.CursorUtil +import org.thoughtcrime.securesms.util.SqlUtil + +/** + * Store reactions on messages. + */ +class ReactionDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Database(context, databaseHelper) { + + companion object { + private const val TABLE_NAME = "reaction" + + private const val ID = "_id" + private const val MESSAGE_ID = "message_id" + private const val IS_MMS = "is_mms" + private const val AUTHOR_ID = "author_id" + private const val EMOJI = "emoji" + private const val DATE_SENT = "date_sent" + private const val DATE_RECEIVED = "date_received" + + @JvmField + val CREATE_TABLE = """ + CREATE TABLE $TABLE_NAME ( + $ID INTEGER PRIMARY KEY, + $MESSAGE_ID INTEGER NOT NULL, + $IS_MMS INTEGER NOT NULL, + $AUTHOR_ID INTEGER NOT NULL REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}) ON DELETE CASCADE, + $EMOJI TEXT NOT NULL, + $DATE_SENT INTEGER NOT NULL, + $DATE_RECEIVED INTEGER NOT NULL, + UNIQUE($MESSAGE_ID, $IS_MMS, $AUTHOR_ID) ON CONFLICT REPLACE + ) + """.trimIndent() + + @JvmField + val CREATE_TRIGGERS = arrayOf( + """ + CREATE TRIGGER reactions_sms_delete AFTER DELETE ON ${SmsDatabase.TABLE_NAME} + BEGIN + DELETE FROM $TABLE_NAME WHERE $MESSAGE_ID = old.${SmsDatabase.ID} AND $IS_MMS = 0; + END + """, + """ + CREATE TRIGGER reactions_mms_delete AFTER DELETE ON ${MmsDatabase.TABLE_NAME} + BEGIN + DELETE FROM $TABLE_NAME WHERE $MESSAGE_ID = old.${MmsDatabase.ID} AND $IS_MMS = 1; + END + """ + ) + + private fun readReaction(cursor: Cursor): ReactionRecord { + return ReactionRecord( + emoji = CursorUtil.requireString(cursor, EMOJI), + author = RecipientId.from(CursorUtil.requireLong(cursor, AUTHOR_ID)), + dateSent = CursorUtil.requireLong(cursor, DATE_SENT), + dateReceived = CursorUtil.requireLong(cursor, DATE_RECEIVED) + ) + } + } + + fun getReactions(messageId: MessageId): List { + val query = "$MESSAGE_ID = ? AND $IS_MMS = ?" + val args = SqlUtil.buildArgs(messageId.id, if (messageId.mms) 1 else 0) + + val reactions: MutableList = mutableListOf() + + databaseHelper.signalReadableDatabase.query(TABLE_NAME, null, query, args, null, null, null).use { cursor -> + while (cursor.moveToNext()) { + reactions += readReaction(cursor) + } + } + + return reactions + } + + fun getReactionsForMessages(messageIds: Collection): Map> { + if (messageIds.isEmpty()) { + return emptyMap() + } + + val messageIdToReactions: MutableMap> = mutableMapOf() + + val args: List> = messageIds.map { SqlUtil.buildArgs(it.id, if (it.mms) 1 else 0) } + + for (query: SqlUtil.Query in SqlUtil.buildCustomCollectionQuery("$MESSAGE_ID = ? AND $IS_MMS = ?", args)) { + databaseHelper.signalReadableDatabase.query(TABLE_NAME, null, query.where, query.whereArgs, null, null, null).use { cursor -> + while (cursor.moveToNext()) { + val reaction: ReactionRecord = readReaction(cursor) + val messageId = MessageId( + id = CursorUtil.requireLong(cursor, MESSAGE_ID), + mms = CursorUtil.requireBoolean(cursor, IS_MMS) + ) + + var reactionsList: MutableList? = messageIdToReactions[messageId] + + if (reactionsList == null) { + reactionsList = mutableListOf() + messageIdToReactions[messageId] = reactionsList + } + + reactionsList.add(reaction) + } + } + } + + return messageIdToReactions + } + + fun addReaction(messageId: MessageId, reaction: ReactionRecord) { + val db: SQLiteDatabase = databaseHelper.signalWritableDatabase + + db.beginTransaction() + try { + val values = ContentValues().apply { + put(MESSAGE_ID, messageId.id) + put(IS_MMS, if (messageId.mms) 1 else 0) + put(EMOJI, reaction.emoji) + put(AUTHOR_ID, reaction.author.serialize()) + put(DATE_SENT, reaction.dateSent) + put(DATE_RECEIVED, reaction.dateReceived) + } + + db.insert(TABLE_NAME, null, values) + + if (messageId.mms) { + DatabaseFactory.getMmsDatabase(context).updateReactionsUnread(db, messageId.id, hasReactions(messageId), false) + } else { + DatabaseFactory.getSmsDatabase(context).updateReactionsUnread(db, messageId.id, hasReactions(messageId), false) + } + + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(messageId) + } + + fun deleteReaction(messageId: MessageId, recipientId: RecipientId) { + val db: SQLiteDatabase = databaseHelper.signalWritableDatabase + + db.beginTransaction() + try { + val query = "$MESSAGE_ID = ? AND $IS_MMS = ? AND $AUTHOR_ID = ?" + val args = SqlUtil.buildArgs(messageId.id, if (messageId.mms) 1 else 0, recipientId) + + db.delete(TABLE_NAME, query, args) + + if (messageId.mms) { + DatabaseFactory.getMmsDatabase(context).updateReactionsUnread(db, messageId.id, hasReactions(messageId), true) + } else { + DatabaseFactory.getSmsDatabase(context).updateReactionsUnread(db, messageId.id, hasReactions(messageId), true) + } + + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + + ApplicationDependencies.getDatabaseObserver().notifyMessageUpdateObservers(messageId) + } + + fun hasReaction(messageId: MessageId, reaction: ReactionRecord): Boolean { + val query = "$MESSAGE_ID = ? AND $IS_MMS = ? AND $AUTHOR_ID = ? AND $EMOJI = ?" + val args = SqlUtil.buildArgs(messageId.id, if (messageId.mms) 1 else 0, reaction.author, reaction.emoji) + + databaseHelper.signalReadableDatabase.query(TABLE_NAME, arrayOf(MESSAGE_ID), query, args, null, null, null).use { cursor -> + return cursor.moveToFirst() + } + } + + private fun hasReactions(messageId: MessageId): Boolean { + val query = "$MESSAGE_ID = ? AND $IS_MMS = ?" + val args = SqlUtil.buildArgs(messageId.id, if (messageId.mms) 1 else 0) + + databaseHelper.signalReadableDatabase.query(TABLE_NAME, arrayOf(MESSAGE_ID), query, args, null, null, null).use { cursor -> + return cursor.moveToFirst() + } + } + + fun remapRecipient(oldAuthorId: RecipientId, newAuthorId: RecipientId) { + val query = "$AUTHOR_ID = ?" + val args = SqlUtil.buildArgs(oldAuthorId) + val values = ContentValues().apply { + put(AUTHOR_ID, newAuthorId.serialize()) + } + + databaseHelper.signalWritableDatabase.update(TABLE_NAME, values, query, args) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index d0f4b5d3a5..cd1bbb9892 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -3019,43 +3019,6 @@ public class RecipientDatabase extends Database { RecipientSettings uuidSettings = getRecipientSettings(byUuid); RecipientSettings e164Settings = getRecipientSettings(byE164); - // Recipient - Log.w(TAG, "Deleting recipient " + byE164, true); - db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164)); - RemappedRecords.getInstance().addRecipient(context, byE164, byUuid); - - ContentValues uuidValues = new ContentValues(); - uuidValues.put(PHONE, e164Settings.getE164()); - uuidValues.put(BLOCKED, e164Settings.isBlocked() || uuidSettings.isBlocked()); - uuidValues.put(MESSAGE_RINGTONE, Optional.fromNullable(uuidSettings.getMessageRingtone()).or(Optional.fromNullable(e164Settings.getMessageRingtone())).transform(Uri::toString).orNull()); - uuidValues.put(MESSAGE_VIBRATE, uuidSettings.getMessageVibrateState() != VibrateState.DEFAULT ? uuidSettings.getMessageVibrateState().getId() : e164Settings.getMessageVibrateState().getId()); - uuidValues.put(CALL_RINGTONE, Optional.fromNullable(uuidSettings.getCallRingtone()).or(Optional.fromNullable(e164Settings.getCallRingtone())).transform(Uri::toString).orNull()); - uuidValues.put(CALL_VIBRATE, uuidSettings.getCallVibrateState() != VibrateState.DEFAULT ? uuidSettings.getCallVibrateState().getId() : e164Settings.getCallVibrateState().getId()); - uuidValues.put(NOTIFICATION_CHANNEL, uuidSettings.getNotificationChannel() != null ? uuidSettings.getNotificationChannel() : e164Settings.getNotificationChannel()); - uuidValues.put(MUTE_UNTIL, uuidSettings.getMuteUntil() > 0 ? uuidSettings.getMuteUntil() : e164Settings.getMuteUntil()); - uuidValues.put(CHAT_COLORS, Optional.fromNullable(uuidSettings.getChatColors()).or(Optional.fromNullable(e164Settings.getChatColors())).transform(colors -> colors.serialize().toByteArray()).orNull()); - uuidValues.put(AVATAR_COLOR, uuidSettings.getAvatarColor().serialize()); - uuidValues.put(CUSTOM_CHAT_COLORS_ID, Optional.fromNullable(uuidSettings.getChatColors()).or(Optional.fromNullable(e164Settings.getChatColors())).transform(colors -> colors.getId().getLongValue()).orNull()); - uuidValues.put(SEEN_INVITE_REMINDER, e164Settings.getInsightsBannerTier().getId()); - uuidValues.put(DEFAULT_SUBSCRIPTION_ID, e164Settings.getDefaultSubscriptionId().or(-1)); - uuidValues.put(MESSAGE_EXPIRATION_TIME, uuidSettings.getExpireMessages() > 0 ? uuidSettings.getExpireMessages() : e164Settings.getExpireMessages()); - uuidValues.put(REGISTERED, RegisteredState.REGISTERED.getId()); - uuidValues.put(SYSTEM_GIVEN_NAME, e164Settings.getSystemProfileName().getGivenName()); - uuidValues.put(SYSTEM_FAMILY_NAME, e164Settings.getSystemProfileName().getFamilyName()); - uuidValues.put(SYSTEM_JOINED_NAME, e164Settings.getSystemProfileName().toString()); - uuidValues.put(SYSTEM_PHOTO_URI, e164Settings.getSystemContactPhotoUri()); - uuidValues.put(SYSTEM_PHONE_LABEL, e164Settings.getSystemPhoneLabel()); - uuidValues.put(SYSTEM_CONTACT_URI, e164Settings.getSystemContactUri()); - uuidValues.put(PROFILE_SHARING, uuidSettings.isProfileSharing() || e164Settings.isProfileSharing()); - uuidValues.put(CAPABILITIES, Math.max(uuidSettings.getCapabilities(), e164Settings.getCapabilities())); - uuidValues.put(MENTION_SETTING, uuidSettings.getMentionSetting() != MentionSetting.ALWAYS_NOTIFY ? uuidSettings.getMentionSetting().getId() : e164Settings.getMentionSetting().getId()); - if (uuidSettings.getProfileKey() != null) { - updateProfileValuesForMerge(uuidValues, uuidSettings); - } else if (e164Settings.getProfileKey() != null) { - updateProfileValuesForMerge(uuidValues, e164Settings); - } - db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(byUuid)); - // Identities ApplicationDependencies.getIdentityStore().delete(e164Settings.e164); @@ -3141,6 +3104,46 @@ public class RecipientDatabase extends Database { DatabaseFactory.getThreadDatabase(context).setLastScrolled(threadMerge.threadId, 0); DatabaseFactory.getThreadDatabase(context).update(threadMerge.threadId, false, false); + // Reactions + DatabaseFactory.getReactionDatabase(context).remapRecipient(byE164, byUuid); + + // Recipient + Log.w(TAG, "Deleting recipient " + byE164, true); + db.delete(TABLE_NAME, ID_WHERE, SqlUtil.buildArgs(byE164)); + RemappedRecords.getInstance().addRecipient(context, byE164, byUuid); + + ContentValues uuidValues = new ContentValues(); + uuidValues.put(PHONE, e164Settings.getE164()); + uuidValues.put(BLOCKED, e164Settings.isBlocked() || uuidSettings.isBlocked()); + uuidValues.put(MESSAGE_RINGTONE, Optional.fromNullable(uuidSettings.getMessageRingtone()).or(Optional.fromNullable(e164Settings.getMessageRingtone())).transform(Uri::toString).orNull()); + uuidValues.put(MESSAGE_VIBRATE, uuidSettings.getMessageVibrateState() != VibrateState.DEFAULT ? uuidSettings.getMessageVibrateState().getId() : e164Settings.getMessageVibrateState().getId()); + uuidValues.put(CALL_RINGTONE, Optional.fromNullable(uuidSettings.getCallRingtone()).or(Optional.fromNullable(e164Settings.getCallRingtone())).transform(Uri::toString).orNull()); + uuidValues.put(CALL_VIBRATE, uuidSettings.getCallVibrateState() != VibrateState.DEFAULT ? uuidSettings.getCallVibrateState().getId() : e164Settings.getCallVibrateState().getId()); + uuidValues.put(NOTIFICATION_CHANNEL, uuidSettings.getNotificationChannel() != null ? uuidSettings.getNotificationChannel() : e164Settings.getNotificationChannel()); + uuidValues.put(MUTE_UNTIL, uuidSettings.getMuteUntil() > 0 ? uuidSettings.getMuteUntil() : e164Settings.getMuteUntil()); + uuidValues.put(CHAT_COLORS, Optional.fromNullable(uuidSettings.getChatColors()).or(Optional.fromNullable(e164Settings.getChatColors())).transform(colors -> colors.serialize().toByteArray()).orNull()); + uuidValues.put(AVATAR_COLOR, uuidSettings.getAvatarColor().serialize()); + uuidValues.put(CUSTOM_CHAT_COLORS_ID, Optional.fromNullable(uuidSettings.getChatColors()).or(Optional.fromNullable(e164Settings.getChatColors())).transform(colors -> colors.getId().getLongValue()).orNull()); + uuidValues.put(SEEN_INVITE_REMINDER, e164Settings.getInsightsBannerTier().getId()); + uuidValues.put(DEFAULT_SUBSCRIPTION_ID, e164Settings.getDefaultSubscriptionId().or(-1)); + uuidValues.put(MESSAGE_EXPIRATION_TIME, uuidSettings.getExpireMessages() > 0 ? uuidSettings.getExpireMessages() : e164Settings.getExpireMessages()); + uuidValues.put(REGISTERED, RegisteredState.REGISTERED.getId()); + uuidValues.put(SYSTEM_GIVEN_NAME, e164Settings.getSystemProfileName().getGivenName()); + uuidValues.put(SYSTEM_FAMILY_NAME, e164Settings.getSystemProfileName().getFamilyName()); + uuidValues.put(SYSTEM_JOINED_NAME, e164Settings.getSystemProfileName().toString()); + uuidValues.put(SYSTEM_PHOTO_URI, e164Settings.getSystemContactPhotoUri()); + uuidValues.put(SYSTEM_PHONE_LABEL, e164Settings.getSystemPhoneLabel()); + uuidValues.put(SYSTEM_CONTACT_URI, e164Settings.getSystemContactUri()); + uuidValues.put(PROFILE_SHARING, uuidSettings.isProfileSharing() || e164Settings.isProfileSharing()); + uuidValues.put(CAPABILITIES, Math.max(uuidSettings.getCapabilities(), e164Settings.getCapabilities())); + uuidValues.put(MENTION_SETTING, uuidSettings.getMentionSetting() != MentionSetting.ALWAYS_NOTIFY ? uuidSettings.getMentionSetting().getId() : e164Settings.getMentionSetting().getId()); + if (uuidSettings.getProfileKey() != null) { + updateProfileValuesForMerge(uuidValues, uuidSettings); + } else if (e164Settings.getProfileKey() != null) { + updateProfileValuesForMerge(uuidValues, e164Settings); + } + db.update(TABLE_NAME, uuidValues, ID_WHERE, SqlUtil.buildArgs(byUuid)); + return byUuid; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 6f54b92095..76bf4842fd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -123,7 +123,6 @@ public class SmsDatabase extends MessageDatabase { NOTIFIED + " DEFAULT 0, " + READ_RECEIPT_COUNT + " INTEGER DEFAULT 0, " + UNIDENTIFIED + " INTEGER DEFAULT 0, " + - REACTIONS + " BLOB DEFAULT NULL, " + REACTIONS_UNREAD + " INTEGER DEFAULT 0, " + REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " + REMOTE_DELETED + " INTEGER DEFAULT 0, " + @@ -148,7 +147,7 @@ public class SmsDatabase extends MessageDatabase { PROTOCOL, READ, STATUS, TYPE, REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, DELIVERY_RECEIPT_COUNT, MISMATCHED_IDENTITIES, SUBSCRIPTION_ID, EXPIRES_IN, EXPIRE_STARTED, - NOTIFIED, READ_RECEIPT_COUNT, UNIDENTIFIED, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN, + NOTIFIED, READ_RECEIPT_COUNT, UNIDENTIFIED, REACTIONS_UNREAD, REACTIONS_LAST_SEEN, REMOTE_DELETED, NOTIFIED_TIMESTAMP, RECEIPT_TIMESTAMP }; @@ -391,7 +390,6 @@ public class SmsDatabase extends MessageDatabase { ContentValues values = new ContentValues(); values.put(REMOTE_DELETED, 1); values.putNull(BODY); - values.putNull(REACTIONS); db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(id) }); threadId = getThreadIdForMessage(id); @@ -1696,7 +1694,6 @@ public class SmsDatabase extends MessageDatabase { String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY)); boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.UNIDENTIFIED)) == 1; boolean remoteDelete = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.REMOTE_DELETED)) == 1; - List reactions = parseReactions(cursor); long notifiedTimestamp = CursorUtil.requireLong(cursor, NOTIFIED_TIMESTAMP); long receiptTimestamp = CursorUtil.requireLong(cursor, RECEIPT_TIMESTAMP); @@ -1713,7 +1710,7 @@ public class SmsDatabase extends MessageDatabase { dateSent, dateReceived, dateServer, deliveryReceiptCount, type, threadId, status, mismatches, subscriptionId, expiresIn, expireStarted, - readReceiptCount, unidentified, reactions, remoteDelete, + readReceiptCount, unidentified, Collections.emptyList(), remoteDelete, notifiedTimestamp, receiptTimestamp); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 836eaa162c..c75654b5a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase; import org.thoughtcrime.securesms.database.PaymentDatabase; import org.thoughtcrime.securesms.database.PendingRetryReceiptDatabase; import org.thoughtcrime.securesms.database.PushDatabase; +import org.thoughtcrime.securesms.database.ReactionDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RemappedRecordsDatabase; import org.thoughtcrime.securesms.database.SearchDatabase; @@ -219,8 +220,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab private static final int BADGES = 118; private static final int SENDER_KEY_UUID = 119; private static final int SENDER_KEY_SHARED_TIMESTAMP = 120; + private static final int REACTION_REFACTOR = 121; - private static final int DATABASE_VERSION = 120; + private static final int DATABASE_VERSION = 121; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -263,6 +265,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab db.execSQL(EmojiSearchDatabase.CREATE_TABLE); db.execSQL(AvatarPickerDatabase.CREATE_TABLE); db.execSQL(GroupCallRingDatabase.CREATE_TABLE); + db.execSQL(ReactionDatabase.CREATE_TABLE); executeStatements(db, SearchDatabase.CREATE_TABLE); executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE); executeStatements(db, MessageSendLogDatabase.CREATE_TABLE); @@ -283,6 +286,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab executeStatements(db, GroupCallRingDatabase.CREATE_INDEXES); executeStatements(db, MessageSendLogDatabase.CREATE_TRIGGERS); + executeStatements(db, ReactionDatabase.CREATE_TRIGGERS); if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) { ClassicOpenHelper legacyHelper = new ClassicOpenHelper(context); @@ -2054,12 +2058,12 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab long start = System.currentTimeMillis(); db.execSQL("CREATE TABLE sender_keys_tmp (_id INTEGER PRIMARY KEY AUTOINCREMENT, " + - "address TEXT NOT NULL, " + - "device INTEGER NOT NULL, " + - "distribution_id TEXT NOT NULL, " + - "record BLOB NOT NULL, " + - "created_at INTEGER NOT NULL, " + - "UNIQUE(address, device, distribution_id) ON CONFLICT REPLACE)"); + "address TEXT NOT NULL, " + + "device INTEGER NOT NULL, " + + "distribution_id TEXT NOT NULL, " + + "record BLOB NOT NULL, " + + "created_at INTEGER NOT NULL, " + + "UNIQUE(address, device, distribution_id) ON CONFLICT REPLACE)"); db.execSQL("INSERT INTO sender_keys_tmp (address, device, distribution_id, record, created_at) " + "SELECT recipient.uuid AS new_address, " + @@ -2080,6 +2084,37 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab db.execSQL("ALTER TABLE sender_key_shared ADD COLUMN timestamp INTEGER DEFAULT 0"); } + if (oldVersion < REACTION_REFACTOR) { + db.execSQL("CREATE TABLE reaction (_id INTEGER PRIMARY KEY, " + + "message_id INTEGER NOT NULL, " + + "is_mms INTEGER NOT NULL, " + + "author_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE, " + + "emoji TEXT NOT NULL, " + + "date_sent INTEGER NOT NULL, " + + "date_received INTEGER NOT NULL, " + + "UNIQUE(message_id, is_mms, author_id) ON CONFLICT REPLACE)"); + + try (Cursor cursor = db.rawQuery("SELECT _id, reactions FROM sms WHERE reactions NOT NULL", null)) { + while (cursor.moveToNext()) { + migrateReaction(db, cursor, false); + } + } + + try (Cursor cursor = db.rawQuery("SELECT _id, reactions FROM mms WHERE reactions NOT NULL", null)) { + while (cursor.moveToNext()) { + migrateReaction(db, cursor, true); + } + } + + db.execSQL("UPDATE reaction SET author_id = IFNULL((SELECT new_id FROM remapped_recipients WHERE author_id = old_id), author_id)"); + + db.execSQL("CREATE TRIGGER reactions_sms_delete AFTER DELETE ON sms BEGIN DELETE FROM reaction WHERE message_id = old._id AND is_mms = 0; END"); + db.execSQL("CREATE TRIGGER reactions_mms_delete AFTER DELETE ON mms BEGIN DELETE FROM reaction WHERE message_id = old._id AND is_mms = 0; END"); + + db.execSQL("UPDATE sms SET reactions = NULL WHERE reactions NOT NULL"); + db.execSQL("UPDATE mms SET reactions = NULL WHERE reactions NOT NULL"); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); @@ -2092,6 +2127,27 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab Log.i(TAG, "Upgrade complete. Took " + (System.currentTimeMillis() - startTime) + " ms."); } + private void migrateReaction(@NonNull SQLiteDatabase db, @NonNull Cursor cursor, boolean isMms) { + try { + long messageId = CursorUtil.requireLong(cursor, "_id"); + ReactionList reactionList = ReactionList.parseFrom(CursorUtil.requireBlob(cursor, "reactions")); + + for (ReactionList.Reaction reaction : reactionList.getReactionsList()) { + ContentValues contentValues = new ContentValues(); + contentValues.put("message_id", messageId); + contentValues.put("is_mms", isMms ? 1 : 0); + contentValues.put("author_id", reaction.getAuthor()); + contentValues.put("emoji", reaction.getEmoji()); + contentValues.put("date_sent", reaction.getSentTime()); + contentValues.put("date_received", reaction.getReceivedTime()); + + db.insert("reaction", null, contentValues); + } + } catch (InvalidProtocolBufferException e) { + Log.w(TAG, "Failed to parse reaction!"); + } + } + @Override public net.zetetic.database.sqlcipher.SQLiteDatabase getReadableDatabase() { throw new UnsupportedOperationException("Call getSignalReadableDatabase() instead!"); @@ -2099,7 +2155,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab @Override public net.zetetic.database.sqlcipher.SQLiteDatabase getWritableDatabase() { - throw new UnsupportedOperationException("Call getSignalReadableDatabase() instead!"); + throw new UnsupportedOperationException("Call getSignalWritableDatabase() instead!"); } public net.zetetic.database.sqlcipher.SQLiteDatabase getRawReadableDatabase() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java index 35562e8104..135732b98d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java @@ -128,6 +128,13 @@ public class MediaMmsMessageRecord extends MmsMessageRecord { return partCount; } + public @NonNull MediaMmsMessageRecord withReactions(@NonNull List reactions) { + return new MediaMmsMessageRecord(getId(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(), + getPartCount(), getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(), + getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, isRemoteDelete(), mentionsSelf, + getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp()); + } + public @NonNull MediaMmsMessageRecord withAttachments(@NonNull Context context, @NonNull List attachments) { Map attachmentIdMap = new HashMap<>(); for (DatabaseAttachment attachment : attachments) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ReactionRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ReactionRecord.java deleted file mode 100644 index f333362a42..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ReactionRecord.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.thoughtcrime.securesms.database.model; - -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.recipients.RecipientId; - -import java.util.Objects; - -public class ReactionRecord { - private final String emoji; - private final RecipientId author; - private final long dateSent; - private final long dateReceived; - - public ReactionRecord(@NonNull String emoji, - @NonNull RecipientId author, - long dateSent, - long dateReceived) - { - this.emoji = emoji; - this.author = author; - this.dateSent = dateSent; - this.dateReceived = dateReceived; - } - - public @NonNull String getEmoji() { - return emoji; - } - - public @NonNull RecipientId getAuthor() { - return author; - } - - public long getDateSent() { - return dateSent; - } - - public long getDateReceived() { - return dateReceived; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ReactionRecord that = (ReactionRecord) o; - return dateSent == that.dateSent && - dateReceived == that.dateReceived && - Objects.equals(emoji, that.emoji) && - Objects.equals(author, that.author); - } - - @Override - public int hashCode() { - return Objects.hash(emoji, author, dateSent, dateReceived); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ReactionRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/ReactionRecord.kt new file mode 100644 index 0000000000..a0386e9e0d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ReactionRecord.kt @@ -0,0 +1,13 @@ +package org.thoughtcrime.securesms.database.model + +import org.thoughtcrime.securesms.recipients.RecipientId + +/** + * Represents an individual reaction to a message. + */ +data class ReactionRecord( + val emoji: String, + val author: RecipientId, + val dateSent: Long, + val dateReceived: Long +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java index ceb658186c..7f6a9546ef 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java @@ -109,4 +109,11 @@ public class SmsMessageRecord extends MessageRecord { public boolean isMmsNotification() { return false; } + + public @NonNull SmsMessageRecord withReactions(@NonNull List reactions) { + return new SmsMessageRecord(getId(), getBody(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(), + getServerTimestamp(), getDeliveryReceiptCount(), getType(), getThreadId(), getDeliveryStatus(), getIdentityKeyMismatches(), + getSubscriptionId(), getExpiresIn(), getExpireStarted(), getReadReceiptCount(), isUnidentified(), reactions, isRemoteDelete(), + getNotifiedTimestamp(), getReceiptTimestamp()); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java index d15d0cf2a5..aaff470dff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java @@ -9,6 +9,7 @@ import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; +import org.thoughtcrime.securesms.database.ReactionDatabase; import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.ReactionRecord; @@ -50,8 +51,7 @@ public class ReactionSendJob extends BaseJob { private static final String KEY_RECIPIENTS = "recipients"; private static final String KEY_INITIAL_RECIPIENT_COUNT = "initial_recipient_count"; - private final long messageId; - private final boolean isMms; + private final MessageId messageId; private final List recipients; private final int initialRecipientCount; private final ReactionRecord reaction; @@ -60,14 +60,13 @@ public class ReactionSendJob extends BaseJob { @WorkerThread public static @NonNull ReactionSendJob create(@NonNull Context context, - long messageId, - boolean isMms, + @NonNull MessageId messageId, @NonNull ReactionRecord reaction, boolean remove) throws NoSuchMessageException { - MessageRecord message = isMms ? DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId) - : DatabaseFactory.getSmsDatabase(context).getSmsMessage(messageId); + MessageRecord message = messageId.isMms() ? DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId.getId()) + : DatabaseFactory.getSmsDatabase(context).getSmsMessage(messageId.getId()); Recipient conversationRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(message.getThreadId()); @@ -84,7 +83,6 @@ public class ReactionSendJob extends BaseJob { : Collections.singletonList(conversationRecipient.getId()); return new ReactionSendJob(messageId, - isMms, recipients, recipients.size(), reaction, @@ -96,8 +94,7 @@ public class ReactionSendJob extends BaseJob { .build()); } - private ReactionSendJob(long messageId, - boolean isMms, + private ReactionSendJob(@NonNull MessageId messageId, @NonNull List recipients, int initialRecipientCount, @NonNull ReactionRecord reaction, @@ -107,7 +104,6 @@ public class ReactionSendJob extends BaseJob { super(parameters); this.messageId = messageId; - this.isMms = isMms; this.recipients = recipients; this.initialRecipientCount = initialRecipientCount; this.reaction = reaction; @@ -116,8 +112,8 @@ public class ReactionSendJob extends BaseJob { @Override public @NonNull Data serialize() { - return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId) - .putBoolean(KEY_IS_MMS, isMms) + return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId.getId()) + .putBoolean(KEY_IS_MMS, messageId.isMms()) .putString(KEY_REACTION_EMOJI, reaction.getEmoji()) .putString(KEY_REACTION_AUTHOR, reaction.getAuthor().serialize()) .putLong(KEY_REACTION_DATE_SENT, reaction.getDateSent()) @@ -139,26 +135,25 @@ public class ReactionSendJob extends BaseJob { throw new NotPushRegisteredException(); } - MessageDatabase db; - MessageRecord message; + ReactionDatabase reactionDatabase = DatabaseFactory.getReactionDatabase(context); - if (isMms) { - db = DatabaseFactory.getMmsDatabase(context); - message = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId); + MessageRecord message; + + if (messageId.isMms()) { + message = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId.getId()); } else { - db = DatabaseFactory.getSmsDatabase(context); - message = DatabaseFactory.getSmsDatabase(context).getSmsMessage(messageId); + message = DatabaseFactory.getSmsDatabase(context).getSmsMessage(messageId.getId()); } Recipient targetAuthor = message.isOutgoing() ? Recipient.self() : message.getIndividualRecipient(); long targetSentTimestamp = message.getDateSent(); - if (!remove && !db.hasReaction(messageId, reaction)) { + if (!remove && !reactionDatabase.hasReaction(messageId, reaction)) { Log.w(TAG, "Went to add a reaction, but it's no longer present on the message!"); return; } - if (remove && db.hasReaction(messageId, reaction)) { + if (remove && reactionDatabase.hasReaction(messageId, reaction)) { Log.w(TAG, "Went to remove a reaction, but it's still there!"); return; } @@ -207,14 +202,14 @@ public class ReactionSendJob extends BaseJob { Log.w(TAG, "Failed to send the reaction to all recipients!"); - MessageDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context); + ReactionDatabase reactionDatabase = DatabaseFactory.getReactionDatabase(context); - if (remove && !db.hasReaction(messageId, reaction)) { + if (remove && !reactionDatabase.hasReaction(messageId, reaction)) { Log.w(TAG, "Reaction removal failed, so adding the reaction back."); - db.addReaction(messageId, reaction); - } else if (!remove && db.hasReaction(messageId, reaction)){ + reactionDatabase.addReaction(messageId, reaction); + } else if (!remove && reactionDatabase.hasReaction(messageId, reaction)){ Log.w(TAG, "Reaction addition failed, so removing the reaction."); - db.deleteReaction(messageId, reaction.getAuthor()); + reactionDatabase.deleteReaction(messageId, reaction.getAuthor()); } else { Log.w(TAG, "Reaction state didn't match what we'd expect to revert it, so we're just leaving it alone."); } @@ -237,7 +232,7 @@ public class ReactionSendJob extends BaseJob { destinations, false, ContentHint.RESENDABLE, - new MessageId(messageId, isMms), + messageId, dataMessage); return GroupSendJobHelper.getCompletedSends(destinations, results); @@ -271,7 +266,7 @@ public class ReactionSendJob extends BaseJob { data.getLong(KEY_REACTION_DATE_RECEIVED)); boolean remove = data.getBoolean(KEY_REMOVE); - return new ReactionSendJob(messageId, isMms, recipients, initialRecipientCount, reaction, remove, parameters); + return new ReactionSendJob(new MessageId(messageId, isMms), recipients, initialRecipientCount, reaction, remove, parameters); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index 32c2923040..b882939968 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -874,14 +874,14 @@ public final class MessageContentProcessor { return null; } - MessageDatabase db = targetMessage.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context); + MessageId targetMessageId = new MessageId(targetMessage.getId(), targetMessage.isMms()); if (reaction.isRemove()) { - db.deleteReaction(targetMessage.getId(), senderRecipient.getId()); + DatabaseFactory.getReactionDatabase(context).deleteReaction(targetMessageId, senderRecipient.getId()); ApplicationDependencies.getMessageNotifier().updateNotification(context); } else { ReactionRecord reactionRecord = new ReactionRecord(reaction.getEmoji(), senderRecipient.getId(), message.getTimestamp(), System.currentTimeMillis()); - db.addReaction(targetMessage.getId(), reactionRecord); + DatabaseFactory.getReactionDatabase(context).addReaction(targetMessageId, reactionRecord); ApplicationDependencies.getMessageNotifier().updateNotification(context, targetMessage.getThreadId(), false); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 13ccb6015a..166b8c599f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -89,9 +89,10 @@ public class ApplicationMigrations { static final int CHANGE_NUMBER_CAPABILITY = 45; static final int CHANGE_NUMBER_CAPABILITY_2 = 46; static final int DEFAULT_REACTIONS_SYNC = 47; + static final int DB_REACTIONS_MIGRATION = 48; } - public static final int CURRENT_VERSION = 47; + public static final int CURRENT_VERSION = 48; /** * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call @@ -389,6 +390,10 @@ public class ApplicationMigrations { jobs.put(Version.DEFAULT_REACTIONS_SYNC, new StorageServiceMigrationJob()); } + if (lastSeenVersion < Version.DB_REACTIONS_MIGRATION) { + jobs.put(Version.DB_REACTIONS_MIGRATION, new DatabaseMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt index 018625401c..f9a3db6b53 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt @@ -7,6 +7,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory import org.thoughtcrime.securesms.database.MmsSmsColumns import org.thoughtcrime.securesms.database.MmsSmsDatabase import org.thoughtcrime.securesms.database.RecipientDatabase +import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.ReactionRecord import org.thoughtcrime.securesms.recipients.Recipient @@ -34,13 +35,16 @@ object NotificationStateProvider { while (record != null) { val threadRecipient: Recipient? = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(record.threadId) if (threadRecipient != null) { + val hasUnreadReactions = CursorUtil.requireInt(unreadMessages, MmsSmsColumns.REACTIONS_UNREAD) == 1 + messages += NotificationMessage( messageRecord = record, + reactions = if (hasUnreadReactions) DatabaseFactory.getReactionDatabase(context).getReactions(MessageId(record.id, record.isMms)) else emptyList(), threadRecipient = threadRecipient, threadId = record.threadId, stickyThread = stickyThreads.containsKey(record.threadId), isUnreadMessage = CursorUtil.requireInt(unreadMessages, MmsSmsColumns.READ) == 0, - hasUnreadReactions = CursorUtil.requireInt(unreadMessages, MmsSmsColumns.REACTIONS_UNREAD) == 1, + hasUnreadReactions = hasUnreadReactions, lastReactionRead = CursorUtil.requireLong(unreadMessages, MmsSmsColumns.REACTIONS_LAST_SEEN) ) } @@ -66,7 +70,7 @@ object NotificationStateProvider { } if (notification.hasUnreadReactions) { - notification.messageRecord.reactions.filter { notification.includeReaction(it) } + notification.reactions.filter { notification.includeReaction(it) } .forEach { notificationItems.add(ReactionNotification(notification.threadRecipient, notification.messageRecord, it)) } } } @@ -87,6 +91,7 @@ object NotificationStateProvider { private data class NotificationMessage( val messageRecord: MessageRecord, + val reactions: List, val threadRecipient: Recipient, val threadId: Long, val stickyThread: Boolean, diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.java deleted file mode 100644 index 8d8137c574..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.thoughtcrime.securesms.reactions; - -import androidx.annotation.NonNull; - -import org.thoughtcrime.securesms.recipients.Recipient; - -public class ReactionDetails { - private final Recipient sender; - private final String baseEmoji; - private final String displayEmoji; - private final long timestamp; - - ReactionDetails(@NonNull Recipient sender, @NonNull String baseEmoji, @NonNull String displayEmoji, long timestamp) { - this.sender = sender; - this.baseEmoji = baseEmoji; - this.displayEmoji = displayEmoji; - this.timestamp = timestamp; - } - - public @NonNull Recipient getSender() { - return sender; - } - - public @NonNull String getBaseEmoji() { - return baseEmoji; - } - - public @NonNull String getDisplayEmoji() { - return displayEmoji; - } - - public long getTimestamp() { - return timestamp; - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.kt b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.kt new file mode 100644 index 0000000000..d4584fd53b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.kt @@ -0,0 +1,13 @@ +package org.thoughtcrime.securesms.reactions + +import org.thoughtcrime.securesms.recipients.Recipient + +/** + * A UI model for a reaction in the [ReactionsBottomSheetDialogFragment] + */ +data class ReactionDetails( + val sender: Recipient, + val baseEmoji: String, + val displayEmoji: String, + val timestamp: Long +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsBottomSheetDialogFragment.java index c5c122a073..95b47850ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsBottomSheetDialogFragment.java @@ -13,8 +13,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.view.ViewCompat; import androidx.fragment.app.DialogFragment; -import androidx.lifecycle.ViewModelProviders; -import androidx.loader.app.LoaderManager; +import androidx.lifecycle.ViewModelProvider; import androidx.viewpager2.widget.ViewPager2; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; @@ -23,6 +22,8 @@ import com.google.android.material.tabs.TabLayoutMediator; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.emoji.EmojiImageView; +import org.thoughtcrime.securesms.database.model.MessageId; +import org.thoughtcrime.securesms.util.LifecycleDisposable; import org.thoughtcrime.securesms.util.ThemeUtil; import org.thoughtcrime.securesms.util.ViewUtil; @@ -33,13 +34,13 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF private static final String ARGS_MESSAGE_ID = "reactions.args.message.id"; private static final String ARGS_IS_MMS = "reactions.args.is.mms"; - private long messageId; private ViewPager2 recipientPagerView; - private ReactionsLoader reactionsLoader; private ReactionViewPagerAdapter recipientsAdapter; private ReactionsViewModel viewModel; private Callback callback; + private final LifecycleDisposable disposables = new LifecycleDisposable(); + public static DialogFragment create(long messageId, boolean isMms) { Bundle args = new Bundle(); DialogFragment fragment = new ReactionsBottomSheetDialogFragment(); @@ -61,7 +62,6 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF @Override public void onCreate(@Nullable Bundle savedInstanceState) { - if (ThemeUtil.isDarkTheme(requireContext())) { setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_Signal_BottomSheetDialog_Fixed_ReactWithAny); } else { @@ -82,24 +82,14 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { recipientPagerView = view.findViewById(R.id.reactions_bottom_view_recipient_pager); - messageId = requireArguments().getLong(ARGS_MESSAGE_ID); + + disposables.bindTo(getViewLifecycleOwner()); setUpRecipientsRecyclerView(); setUpTabMediator(savedInstanceState); - reactionsLoader = new ReactionsLoader(requireContext(), - requireArguments().getLong(ARGS_MESSAGE_ID), - requireArguments().getBoolean(ARGS_IS_MMS)); - - LoaderManager.getInstance(requireActivity()).initLoader((int) messageId, null, reactionsLoader); - - setUpViewModel(); - } - - @Override - public void onDestroyView() { - LoaderManager.getInstance(requireActivity()).destroyLoader((int) messageId); - super.onDestroyView(); + MessageId messageId = new MessageId(requireArguments().getLong(ARGS_MESSAGE_ID), requireArguments().getBoolean(ARGS_IS_MMS)); + setUpViewModel(messageId); } @Override @@ -167,16 +157,16 @@ public final class ReactionsBottomSheetDialogFragment extends BottomSheetDialogF recipientPagerView.setAdapter(recipientsAdapter); } - private void setUpViewModel() { - ReactionsViewModel.Factory factory = new ReactionsViewModel.Factory(reactionsLoader); + private void setUpViewModel(@NonNull MessageId messageId) { + ReactionsViewModel.Factory factory = new ReactionsViewModel.Factory(messageId); - viewModel = ViewModelProviders.of(this, factory).get(ReactionsViewModel.class); + viewModel = new ViewModelProvider(this, factory).get(ReactionsViewModel.class); - viewModel.getEmojiCounts().observe(getViewLifecycleOwner(), emojiCounts -> { + disposables.add(viewModel.getEmojiCounts().subscribe(emojiCounts -> { if (emojiCounts.size() <= 1) dismiss(); recipientsAdapter.submitList(emojiCounts); - }); + })); } public interface Callback { diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsLoader.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsLoader.java deleted file mode 100644 index 9a1ef7e6a0..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsLoader.java +++ /dev/null @@ -1,110 +0,0 @@ -package org.thoughtcrime.securesms.reactions; - -import android.content.Context; -import android.database.Cursor; -import android.os.Bundle; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.loader.app.LoaderManager; -import androidx.loader.content.Loader; - -import com.annimon.stream.Stream; - -import org.signal.core.util.concurrent.SignalExecutors; -import org.thoughtcrime.securesms.components.emoji.EmojiUtil; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.MmsDatabase; -import org.thoughtcrime.securesms.database.SmsDatabase; -import org.thoughtcrime.securesms.database.model.MessageRecord; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.AbstractCursorLoader; - -import java.util.Collections; -import java.util.List; - -public class ReactionsLoader implements ReactionsViewModel.Repository, LoaderManager.LoaderCallbacks { - - private final long messageId; - private final boolean isMms; - private final Context appContext; - - private MutableLiveData> internalLiveData = new MutableLiveData<>(); - - public ReactionsLoader(@NonNull Context context, long messageId, boolean isMms) - { - this.messageId = messageId; - this.isMms = isMms; - this.appContext = context.getApplicationContext(); - } - - @Override - public @NonNull Loader onCreateLoader(int id, @Nullable Bundle args) { - return isMms ? new MmsMessageRecordCursorLoader(appContext, messageId) - : new SmsMessageRecordCursorLoader(appContext, messageId); - } - - @Override - public void onLoadFinished(@NonNull Loader loader, Cursor data) { - SignalExecutors.BOUNDED.execute(() -> { - data.moveToPosition(-1); - - MessageRecord record = isMms ? MmsDatabase.readerFor(data).getNext() - : SmsDatabase.readerFor(data).getNext(); - - if (record == null) { - internalLiveData.postValue(Collections.emptyList()); - } else { - internalLiveData.postValue(Stream.of(record.getReactions()) - .map(reactionRecord -> new ReactionDetails(Recipient.resolved(reactionRecord.getAuthor()), - EmojiUtil.getCanonicalRepresentation(reactionRecord.getEmoji()), - reactionRecord.getEmoji(), - reactionRecord.getDateReceived())) - .toList()); - } - }); - } - - @Override - public void onLoaderReset(@NonNull Loader loader) { - // Do nothing? - } - - @Override - public LiveData> getReactions() { - return internalLiveData; - } - - private static final class MmsMessageRecordCursorLoader extends AbstractCursorLoader { - - private final long messageId; - - public MmsMessageRecordCursorLoader(@NonNull Context context, long messageId) { - super(context); - this.messageId = messageId; - } - - @Override - public Cursor getCursor() { - return DatabaseFactory.getMmsDatabase(context).getMessageCursor(messageId); - } - } - - private static final class SmsMessageRecordCursorLoader extends AbstractCursorLoader { - - private final long messageId; - - public SmsMessageRecordCursorLoader(@NonNull Context context, long messageId) { - super(context); - this.messageId = messageId; - } - - @Override - public Cursor getCursor() { - return DatabaseFactory.getSmsDatabase(context).getMessageCursor(messageId); - } - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsRepository.kt new file mode 100644 index 0000000000..83173a738a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsRepository.kt @@ -0,0 +1,48 @@ +package org.thoughtcrime.securesms.reactions + +import android.content.Context +import io.reactivex.rxjava3.core.Observable +import io.reactivex.rxjava3.core.ObservableEmitter +import io.reactivex.rxjava3.schedulers.Schedulers +import org.thoughtcrime.securesms.components.emoji.EmojiUtil +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.DatabaseObserver +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.database.model.ReactionRecord +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.recipients.Recipient + +class ReactionsRepository { + + fun getReactions(messageId: MessageId): Observable> { + return Observable.create { emitter: ObservableEmitter> -> + val databaseObserver: DatabaseObserver = ApplicationDependencies.getDatabaseObserver() + + val messageObserver = DatabaseObserver.MessageObserver { messageId -> + emitter.onNext(fetchReactionDetails(messageId)) + } + + databaseObserver.registerMessageUpdateObserver(messageObserver) + + emitter.setCancellable { + databaseObserver.unregisterObserver(messageObserver) + } + + emitter.onNext(fetchReactionDetails(messageId)) + }.subscribeOn(Schedulers.io()) + } + + private fun fetchReactionDetails(messageId: MessageId): List { + val context: Context = ApplicationDependencies.getApplication() + val reactions: List = DatabaseFactory.getReactionDatabase(context).getReactions(messageId) + + return reactions.map { reaction -> + ReactionDetails( + sender = Recipient.resolved(reaction.author), + baseEmoji = EmojiUtil.getCanonicalRepresentation(reaction.emoji), + displayEmoji = reaction.emoji, + timestamp = reaction.dateReceived + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsViewModel.java index d13a0c316c..4f1a9132ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsViewModel.java @@ -8,32 +8,45 @@ import androidx.lifecycle.ViewModelProvider; import com.annimon.stream.Stream; +import org.thoughtcrime.securesms.database.DatabaseObserver; +import org.thoughtcrime.securesms.database.model.MessageId; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; + +import java.util.Comparator; import java.util.List; import java.util.Map; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.subjects.PublishSubject; + public class ReactionsViewModel extends ViewModel { - private final Repository repository; + private final MessageId messageId; + private final ReactionsRepository repository; - public ReactionsViewModel(@NonNull Repository repository) { - this.repository = repository; + public ReactionsViewModel(@NonNull MessageId messageId) { + this.messageId = messageId; + this.repository = new ReactionsRepository(); } - public @NonNull LiveData> getEmojiCounts() { - return Transformations.map(repository.getReactions(), - reactionList -> { - List emojiCounts = Stream.of(reactionList) - .groupBy(ReactionDetails::getBaseEmoji) - .sorted(this::compareReactions) - .map(entry -> new EmojiCount(entry.getKey(), - getCountDisplayEmoji(entry.getValue()), - entry.getValue())) - .toList(); + public @NonNull Observable> getEmojiCounts() { + return repository.getReactions(messageId) + .map(reactionList -> { + List emojiCounts = Stream.of(reactionList) + .groupBy(ReactionDetails::getBaseEmoji) + .sorted(this::compareReactions) + .map(entry -> new EmojiCount(entry.getKey(), + getCountDisplayEmoji(entry.getValue()), + entry.getValue())) + .toList(); - emojiCounts.add(0, EmojiCount.all(reactionList)); + emojiCounts.add(0, EmojiCount.all(reactionList)); - return emojiCounts; - }); + return emojiCounts; + }) + .observeOn(AndroidSchedulers.mainThread()); } private int compareReactions(@NonNull Map.Entry> lhs, @NonNull Map.Entry> rhs) { @@ -48,7 +61,7 @@ public class ReactionsViewModel extends ViewModel { private long getLatestTimestamp(List reactions) { return Stream.of(reactions) - .max((a, b) -> Long.compare(a.getTimestamp(), b.getTimestamp())) + .max(Comparator.comparingLong(ReactionDetails::getTimestamp)) .map(ReactionDetails::getTimestamp) .orElse(-1L); } @@ -63,21 +76,17 @@ public class ReactionsViewModel extends ViewModel { return reactions.get(reactions.size() - 1).getDisplayEmoji(); } - interface Repository { - LiveData> getReactions(); - } - static final class Factory implements ViewModelProvider.Factory { - private final Repository repository; + private final MessageId messageId; - Factory(@NonNull Repository repository) { - this.repository = repository; + Factory(@NonNull MessageId messageId) { + this.messageId = messageId; } @Override public @NonNull T create(@NonNull Class modelClass) { - return (T) new ReactionsViewModel(repository); + return modelClass.cast(new ReactionsViewModel(messageId)); } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java index f012709925..957235f60f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiBottomSheetDialogFragment.java @@ -39,8 +39,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageCategoriesAdapter; import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageCategoryMappingModel; import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView; -import org.thoughtcrime.securesms.reactions.ReactionsLoader; import org.thoughtcrime.securesms.reactions.edit.EditReactionsActivity; +import org.thoughtcrime.securesms.util.LifecycleDisposable; import org.thoughtcrime.securesms.util.MappingModel; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.ViewUtil; @@ -65,11 +65,12 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee private ReactWithAnyEmojiViewModel viewModel; private Callback callback; - private ReactionsLoader reactionsLoader; private EmojiPageView emojiPageView; private KeyboardPageSearchView search; private View tabBar; + private final LifecycleDisposable disposables = new LifecycleDisposable(); + private final UpdateCategorySelectionOnScroll categoryUpdateOnScroll = new UpdateCategorySelectionOnScroll(); public static DialogFragment createForMessageRecord(@NonNull MessageRecord messageRecord, int startingPage) { @@ -174,11 +175,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - reactionsLoader = new ReactionsLoader(requireContext(), - requireArguments().getLong(ARG_MESSAGE_ID), - requireArguments().getBoolean(ARG_IS_MMS)); - - LoaderManager.getInstance(requireActivity()).initLoader((int) requireArguments().getLong(ARG_MESSAGE_ID), null, reactionsLoader); + disposables.bindTo(getViewLifecycleOwner()); emojiPageView = view.findViewById(R.id.react_with_any_emoji_page_view); emojiPageView.initialize(this, this, true); @@ -219,15 +216,15 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee emojiPageView.addOnScrollListener(new TopAndBottomShadowHelper(requireView().findViewById(R.id.react_with_any_emoji_top_shadow), tabBar.findViewById(R.id.react_with_any_emoji_bottom_shadow))); - viewModel.getEmojiList().observe(getViewLifecycleOwner(), pages -> emojiPageView.setList(pages, null)); - viewModel.getCategories().observe(getViewLifecycleOwner(), categoriesAdapter::submitList); - viewModel.getSelectedKey().observe(getViewLifecycleOwner(), key -> categoriesRecycler.post(() -> { + disposables.add(viewModel.getEmojiList().subscribe(pages -> emojiPageView.setList(pages, null))); + disposables.add(viewModel.getCategories().subscribe(categoriesAdapter::submitList)); + disposables.add(viewModel.getSelectedKey().subscribe(key -> categoriesRecycler.post(() -> { int index = categoriesAdapter.indexOfFirst(EmojiKeyboardPageCategoryMappingModel.class, m -> m.getKey().equals(key)); if (index != -1) { categoriesRecycler.smoothScrollToPosition(index); } - })); + }))); } } @@ -259,7 +256,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee private void initializeViewModel() { Bundle args = requireArguments(); ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext(), args.getString(ARG_RECENT_KEY, REACTION_STORAGE_KEY)); - ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(reactionsLoader, repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS)); + ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS)); viewModel = ViewModelProviders.of(this, factory).get(ReactWithAnyEmojiViewModel.class); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java index 2b8b1c4058..64e8f27538 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java @@ -17,6 +17,7 @@ import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.NoSuchMessageException; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.emoji.EmojiCategory; @@ -68,24 +69,18 @@ final class ReactWithAnyEmojiRepository { return pages; } - void addEmojiToMessage(@NonNull String emoji, long messageId, boolean isMms) { + void addEmojiToMessage(@NonNull String emoji, @NonNull MessageId messageId) { SignalExecutors.BOUNDED.execute(() -> { - try { - MessageDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context); - MessageRecord messageRecord = db.getMessageRecord(messageId); - ReactionRecord oldRecord = Stream.of(messageRecord.getReactions()) - .filter(record -> record.getAuthor().equals(Recipient.self().getId())) - .findFirst() - .orElse(null); + ReactionRecord oldRecord = Stream.of(DatabaseFactory.getReactionDatabase(context).getReactions(messageId)) + .filter(record -> record.getAuthor().equals(Recipient.self().getId())) + .findFirst() + .orElse(null); - if (oldRecord != null && oldRecord.getEmoji().equals(emoji)) { - MessageSender.sendReactionRemoval(context, messageRecord.getId(), messageRecord.isMms(), oldRecord); - } else { - MessageSender.sendNewReaction(context, messageRecord.getId(), messageRecord.isMms(), emoji); - ThreadUtil.runOnMain(() -> recentEmojiPageModel.onCodePointSelected(emoji)); - } - } catch (NoSuchMessageException e) { - Log.w(TAG, "Message not found! Ignoring."); + if (oldRecord != null && oldRecord.getEmoji().equals(emoji)) { + MessageSender.sendReactionRemoval(context, messageId, oldRecord); + } else { + MessageSender.sendNewReaction(context, messageId, emoji); + ThreadUtil.runOnMain(() -> recentEmojiPageModel.onCodePointSelected(emoji)); } }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java index 78fc9b2707..e2d703750c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java @@ -4,28 +4,29 @@ import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Transformations; import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import org.thoughtcrime.securesms.components.emoji.EmojiPageModel; import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter; import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.emoji.EmojiCategory; import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageCategoryMappingModel; import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchRepository; import org.thoughtcrime.securesms.keyvalue.SignalStore; -import org.thoughtcrime.securesms.reactions.ReactionsLoader; +import org.thoughtcrime.securesms.reactions.ReactionsRepository; import org.thoughtcrime.securesms.util.MappingModelList; import org.thoughtcrime.securesms.util.TextSecurePreferences; -import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import java.util.List; import java.util.stream.Collectors; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Observable; +import io.reactivex.rxjava3.subjects.BehaviorSubject; + public final class ReactWithAnyEmojiViewModel extends ViewModel { private static final int SEARCH_LIMIT = 40; @@ -35,13 +36,12 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel { private final boolean isMms; private final EmojiSearchRepository emojiSearchRepository; - private final LiveData categories; - private final LiveData emojiList; - private final MutableLiveData searchResults; - private final MutableLiveData selectedKey; + private final Observable categories; + private final Observable emojiList; + private final BehaviorSubject searchResults; + private final BehaviorSubject selectedKey; - private ReactWithAnyEmojiViewModel(@NonNull ReactionsLoader reactionsLoader, - @NonNull ReactWithAnyEmojiRepository repository, + private ReactWithAnyEmojiViewModel(@NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms, @NonNull EmojiSearchRepository emojiSearchRepository) @@ -50,12 +50,13 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel { this.messageId = messageId; this.isMms = isMms; this.emojiSearchRepository = emojiSearchRepository; - this.searchResults = new MutableLiveData<>(new EmojiSearchResult()); - this.selectedKey = new MutableLiveData<>(getStartingKey()); + this.searchResults = BehaviorSubject.createDefault(new EmojiSearchResult()); + this.selectedKey = BehaviorSubject.createDefault(getStartingKey()); - LiveData> emojiPages = Transformations.map(reactionsLoader.getReactions(), repository::getEmojiPageModels); + Observable> emojiPages = new ReactionsRepository().getReactions(new MessageId(messageId, isMms)) + .map(repository::getEmojiPageModels); - LiveData emojiList = Transformations.map(emojiPages, (pages) -> { + Observable emojiList = emojiPages.map(pages -> { MappingModelList list = new MappingModelList(); for (ReactWithAnyEmojiPage page : pages) { @@ -69,7 +70,7 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel { return list; }); - this.categories = LiveDataUtil.combineLatest(emojiPages, this.selectedKey, (pages, selectedKey) -> { + this.categories = Observable.combineLatest(emojiPages, this.selectedKey.distinctUntilChanged(), (pages, selectedKey) -> { MappingModelList list = new MappingModelList(); list.add(new EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel(RecentEmojiPageModel.KEY.equals(selectedKey))); list.addAll(pages.stream() @@ -82,7 +83,7 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel { return list; }); - this.emojiList = LiveDataUtil.combineLatest(emojiList, searchResults, (all, search) -> { + this.emojiList = Observable.combineLatest(emojiList, searchResults.distinctUntilChanged(), (all, search) -> { if (TextUtils.isEmpty(search.query)) { return all; } else { @@ -94,35 +95,31 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel { }); } - LiveData getCategories() { - return categories; + @NonNull Observable getCategories() { + return categories.observeOn(AndroidSchedulers.mainThread()); } - LiveData getSelectedKey() { - return selectedKey; + @NonNull Observable getSelectedKey() { + return selectedKey.observeOn(AndroidSchedulers.mainThread()); + } + + @NonNull Observable getEmojiList() { + return emojiList.observeOn(AndroidSchedulers.mainThread()); } void onEmojiSelected(@NonNull String emoji) { if (messageId > 0) { SignalStore.emojiValues().setPreferredVariation(emoji); - repository.addEmojiToMessage(emoji, messageId, isMms); + repository.addEmojiToMessage(emoji, new MessageId(messageId, isMms)); } } - LiveData getEmojiList() { - return emojiList; - } - public void onQueryChanged(String query) { - emojiSearchRepository.submitQuery(query, false, SEARCH_LIMIT, m -> searchResults.postValue(new EmojiSearchResult(query, m))); + emojiSearchRepository.submitQuery(query, false, SEARCH_LIMIT, m -> searchResults.onNext(new EmojiSearchResult(query, m))); } public void selectPage(@NonNull String key) { - if (key.equals(selectedKey.getValue())) { - return; - } - - selectedKey.setValue(key); + selectedKey.onNext(key); } private static @NonNull MappingModelList toMappingModels(@NonNull EmojiPageModel model) { @@ -156,22 +153,20 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel { static class Factory implements ViewModelProvider.Factory { - private final ReactionsLoader reactionsLoader; private final ReactWithAnyEmojiRepository repository; private final long messageId; private final boolean isMms; - Factory(@NonNull ReactionsLoader reactionsLoader, @NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) { - this.reactionsLoader = reactionsLoader; - this.repository = repository; - this.messageId = messageId; - this.isMms = isMms; + Factory(@NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) { + this.repository = repository; + this.messageId = messageId; + this.isMms = isMms; } @Override public @NonNull T create(@NonNull Class modelClass) { //noinspection ConstantConditions - return modelClass.cast(new ReactWithAnyEmojiViewModel(reactionsLoader, repository, messageId, isMms, new EmojiSearchRepository(ApplicationDependencies.getApplication()))); + return modelClass.cast(new ReactWithAnyEmojiViewModel(repository, messageId, isMms, new EmojiSearchRepository(ApplicationDependencies.getApplication()))); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java index ef7c95f9bf..ab99d7892f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java @@ -43,6 +43,7 @@ import org.thoughtcrime.securesms.database.NoSuchMessageException; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.database.model.MessageId; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; @@ -320,27 +321,23 @@ public class MessageSender { } } - public static void sendNewReaction(@NonNull Context context, long messageId, boolean isMms, @NonNull String emoji) { - MessageDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context); - ReactionRecord reaction = new ReactionRecord(emoji, Recipient.self().getId(), System.currentTimeMillis(), System.currentTimeMillis()); - - db.addReaction(messageId, reaction); + public static void sendNewReaction(@NonNull Context context, @NonNull MessageId messageId, @NonNull String emoji) { + ReactionRecord reaction = new ReactionRecord(emoji, Recipient.self().getId(), System.currentTimeMillis(), System.currentTimeMillis()); + DatabaseFactory.getReactionDatabase(context).addReaction(messageId, reaction); try { - ApplicationDependencies.getJobManager().add(ReactionSendJob.create(context, messageId, isMms, reaction, false)); + ApplicationDependencies.getJobManager().add(ReactionSendJob.create(context, messageId, reaction, false)); onMessageSent(); } catch (NoSuchMessageException e) { Log.w(TAG, "[sendNewReaction] Could not find message! Ignoring."); } } - public static void sendReactionRemoval(@NonNull Context context, long messageId, boolean isMms, @NonNull ReactionRecord reaction) { - MessageDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context); - - db.deleteReaction(messageId, reaction.getAuthor()); + public static void sendReactionRemoval(@NonNull Context context, @NonNull MessageId messageId, @NonNull ReactionRecord reaction) { + DatabaseFactory.getReactionDatabase(context).deleteReaction(messageId, reaction.getAuthor()); try { - ApplicationDependencies.getJobManager().add(ReactionSendJob.create(context, messageId, isMms, reaction, true)); + ApplicationDependencies.getJobManager().add(ReactionSendJob.create(context, messageId, reaction, true)); onMessageSent(); } catch (NoSuchMessageException e) { Log.w(TAG, "[sendReactionRemoval] Could not find message! Ignoring."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleDisposable.kt b/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleDisposable.kt index 821b69488a..1b634c7118 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleDisposable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleDisposable.kt @@ -12,6 +12,10 @@ import io.reactivex.rxjava3.disposables.Disposable class LifecycleDisposable : DefaultLifecycleObserver { val disposables: CompositeDisposable = CompositeDisposable() + fun bindTo(lifecycleOwner: LifecycleOwner): LifecycleDisposable { + return bindTo(lifecycleOwner.lifecycle) + } + fun bindTo(lifecycle: Lifecycle): LifecycleDisposable { lifecycle.addObserver(this) return this diff --git a/app/src/main/proto/Database.proto b/app/src/main/proto/Database.proto index 1dd1a07280..af5b3bd6ab 100644 --- a/app/src/main/proto/Database.proto +++ b/app/src/main/proto/Database.proto @@ -11,8 +11,10 @@ package signal; option java_package = "org.thoughtcrime.securesms.database.model.databaseprotos"; option java_multiple_files = true; - +// DEPRECATED -- only here for database migrations message ReactionList { + option deprecated = true; + message Reaction { string emoji = 1; uint64 author = 2;