diff --git a/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_00.binproto b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_00.binproto new file mode 100644 index 0000000000..176fa49afd Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_00.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_01.binproto b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_01.binproto new file mode 100644 index 0000000000..43516451fa Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_01.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_02.binproto b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_02.binproto new file mode 100644 index 0000000000..b2e22b3222 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_02.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_03.binproto b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_03.binproto new file mode 100644 index 0000000000..9f7bbfb038 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_03.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_04.binproto b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_04.binproto new file mode 100644 index 0000000000..d067f63143 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_04.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_05.binproto b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_05.binproto new file mode 100644 index 0000000000..f4fef985ae Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_05.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_06.binproto b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_06.binproto new file mode 100644 index 0000000000..59a2ff7284 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_06.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_07.binproto b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_07.binproto new file mode 100644 index 0000000000..e54e0c3aca Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_07.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_08.binproto b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_08.binproto new file mode 100644 index 0000000000..a47942bb4b Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_08.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_09.binproto b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_09.binproto new file mode 100644 index 0000000000..75ab6ef8f8 Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_09.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_10.binproto b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_10.binproto new file mode 100644 index 0000000000..b82372ec1d Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_10.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_11.binproto b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_11.binproto new file mode 100644 index 0000000000..6ab5a7408d Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_11.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_12.binproto b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_12.binproto new file mode 100644 index 0000000000..67dc03577a Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_12.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_13.binproto b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_13.binproto new file mode 100644 index 0000000000..10da98f98b Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_13.binproto differ diff --git a/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_14.binproto b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_14.binproto new file mode 100644 index 0000000000..a553ac9fef Binary files /dev/null and b/app/src/androidTest/assets/backupTests/chat_item_direct_story_reply_14.binproto differ diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt index 06c3c4fd74..2530d65631 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/backup/v2/ArchiveImportExportTests.kt @@ -76,6 +76,11 @@ class ArchiveImportExportTests { runTests { it.startsWith("chat_item_contact_message_") } } +// @Test + fun chatItemDirectStoryReplyMessage() { + runTests { it.startsWith("chat_item_direct_story_reply_") } + } + // @Test fun chatItemExpirationTimerUpdate() { runTests { it.startsWith("chat_item_expiration_timer_update_") } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt index 005bcffdea..f2e62e926c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ArchiveErrorCases.kt @@ -43,6 +43,10 @@ object ExportSkips { return log(sentTimestamp, "Group update record is parseable, but has no updates.") } + fun directStoryReplyHasNoBody(sentTimestamp: Long): String { + return log(sentTimestamp, "Direct story reply has no body.") + } + private fun log(sentTimestamp: Long, message: String): String { return "[SKIP][$sentTimestamp] $message" } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableArchiveExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableArchiveExtensions.kt index dd05438996..45bc88ba4b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableArchiveExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/MessageTableArchiveExtensions.kt @@ -59,7 +59,8 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, medi ${MessageTable.MISMATCHED_IDENTITIES}, ${MessageTable.TYPE}, ${MessageTable.MESSAGE_EXTRAS}, - ${MessageTable.VIEW_ONCE} + ${MessageTable.VIEW_ONCE}, + ${MessageTable.PARENT_STORY_ID} ) """.trimMargin() ) @@ -123,7 +124,8 @@ fun MessageTable.getMessagesForBackup(db: SignalDatabase, backupTime: Long, medi MessageTable.MISMATCHED_IDENTITIES, MessageTable.TYPE, MessageTable.MESSAGE_EXTRAS, - MessageTable.VIEW_ONCE + MessageTable.VIEW_ONCE, + MessageTable.PARENT_STORY_ID ) .from("${MessageTable.TABLE_NAME} INDEXED BY $dateReceivedIndex") .where("${MessageTable.STORY_TYPE} = 0 AND ${MessageTable.DATE_RECEIVED} >= $lastSeenReceivedTime") diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt index 276c5357ef..de391bc341 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/exporters/ChatItemArchiveExporter.kt @@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.ChatItem import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage import org.thoughtcrime.securesms.backup.v2.proto.ContactAttachment import org.thoughtcrime.securesms.backup.v2.proto.ContactMessage +import org.thoughtcrime.securesms.backup.v2.proto.DirectStoryReplyMessage import org.thoughtcrime.securesms.backup.v2.proto.ExpirationTimerChatUpdate import org.thoughtcrime.securesms.backup.v2.proto.GenericGroupUpdate import org.thoughtcrime.securesms.backup.v2.proto.GroupCall @@ -291,6 +292,10 @@ class ChatItemArchiveExporter( builder.viewOnceMessage = record.toRemoteViewOnceMessage(mediaArchiveEnabled = mediaArchiveEnabled, reactionRecords = extraData.reactionsById[id], attachments = extraData.attachmentsById[id]) } + record.parentStoryId != 0L -> { + builder.directStoryReplyMessage = record.toRemoteDirectStoryReplyMessage(mediaArchiveEnabled = mediaArchiveEnabled, reactionRecords = extraData.reactionsById[id], attachments = extraData.attachmentsById[record.id]) ?: continue + } + else -> { if (record.body.isNullOrEmpty() && !extraData.attachmentsById.containsKey(record.id)) { Log.w(TAG, ExportSkips.emptyChatItem(record.dateSent)) @@ -852,6 +857,36 @@ private fun Contact.PostalAddress.Type.toRemote(): ContactAttachment.PostalAddre } } +private fun BackupMessageRecord.toRemoteDirectStoryReplyMessage(mediaArchiveEnabled: Boolean, reactionRecords: List?, attachments: List?): DirectStoryReplyMessage? { + if (this.body.isNullOrBlank()) { + Log.w(TAG, ExportSkips.directStoryReplyHasNoBody(this.dateSent)) + return null + } + + val isReaction = MessageTypes.isStoryReaction(this.type) + + return DirectStoryReplyMessage( + storySentTimestamp = this.parentStoryId.takeUnless { it == MessageTable.PARENT_STORY_MISSING_ID }, + emoji = if (isReaction) { + this.body + } else { + null + }, + textReply = if (!isReaction) { + DirectStoryReplyMessage.TextReply( + text = Text( + body = this.body, + bodyRanges = this.bodyRanges?.toRemoteBodyRanges(this.dateSent) ?: emptyList() + ), + longText = attachments?.firstOrNull { it.contentType == MediaUtil.LONG_TEXT }?.toRemoteFilePointer(mediaArchiveEnabled) + ) + } else { + null + }, + reactions = reactionRecords.toRemote() + ) +} + private fun BackupMessageRecord.toRemoteStandardMessage(db: SignalDatabase, mediaArchiveEnabled: Boolean, reactionRecords: List?, mentions: List?, attachments: List?): StandardMessage { val text = body?.let { Text( @@ -1344,7 +1379,8 @@ private fun Cursor.toBackupMessageRecord(pastIds: Set, backupStartTime: Lo identityMismatchRecipientIds = this.requireString(MessageTable.MISMATCHED_IDENTITIES).parseIdentityMismatches(), baseType = this.requireLong(MessageTable.TYPE) and MessageTypes.BASE_TYPE_MASK, messageExtras = this.requireBlob(MessageTable.MESSAGE_EXTRAS).parseMessageExtras(), - viewOnce = this.requireBoolean(MessageTable.VIEW_ONCE) + viewOnce = this.requireBoolean(MessageTable.VIEW_ONCE), + parentStoryId = this.requireLong(MessageTable.PARENT_STORY_ID) ) } @@ -1372,6 +1408,7 @@ private class BackupMessageRecord( val quoteBodyRanges: ByteArray?, val quoteType: Int, val originalMessageId: Long?, + val parentStoryId: Long, val latestRevisionId: Long?, val hasDeliveryReceipt: Boolean, val hasReadReceipt: Boolean, diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ChatItemArchiveImporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ChatItemArchiveImporter.kt index cbba360def..ce0301b39c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ChatItemArchiveImporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ChatItemArchiveImporter.kt @@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.backup.v2.proto.BodyRange import org.thoughtcrime.securesms.backup.v2.proto.ChatItem import org.thoughtcrime.securesms.backup.v2.proto.ChatUpdateMessage import org.thoughtcrime.securesms.backup.v2.proto.ContactAttachment +import org.thoughtcrime.securesms.backup.v2.proto.DirectStoryReplyMessage import org.thoughtcrime.securesms.backup.v2.proto.GroupCall import org.thoughtcrime.securesms.backup.v2.proto.IndividualCall import org.thoughtcrime.securesms.backup.v2.proto.LinkPreview @@ -125,7 +126,8 @@ class ChatItemArchiveImporter( MessageTable.VIEW_ONCE, MessageTable.MESSAGE_EXTRAS, MessageTable.ORIGINAL_MESSAGE_ID, - MessageTable.LATEST_REVISION_ID + MessageTable.LATEST_REVISION_ID, + MessageTable.PARENT_STORY_ID ) private val REACTION_COLUMNS = arrayOf( @@ -238,10 +240,11 @@ class ChatItemArchiveImporter( private fun ChatItem.toMessageInsert(fromRecipientId: RecipientId, chatRecipientId: RecipientId, threadId: Long): MessageInsert { val contentValues = this.toMessageContentValues(fromRecipientId, chatRecipientId, threadId) - var followUp: ((Long) -> Unit)? = null + val followUps: MutableList<(Long) -> Unit> = mutableListOf() + if (this.updateMessage != null) { if (this.updateMessage.individualCall != null && this.updateMessage.individualCall.callId != null) { - followUp = { messageRowId -> + followUps += { messageRowId -> val values = contentValuesOf( CallTable.CALL_ID to updateMessage.individualCall.callId, CallTable.MESSAGE_ID to messageRowId, @@ -263,7 +266,7 @@ class ChatItemArchiveImporter( db.insert(CallTable.TABLE_NAME, SQLiteDatabase.CONFLICT_IGNORE, values) } } else if (this.updateMessage.groupCall != null && this.updateMessage.groupCall.callId != null) { - followUp = { messageRowId -> + followUps += { messageRowId -> val ringer: RecipientId? = this.updateMessage.groupCall.ringerRecipientId?.let { importState.remoteToLocalRecipientId[it] } val values = contentValuesOf( @@ -295,7 +298,7 @@ class ChatItemArchiveImporter( } if (this.paymentNotification != null) { - followUp = { messageRowId -> + followUps += { messageRowId -> val uuid = tryRestorePayment(this, chatRecipientId) if (uuid != null) { db.update(MessageTable.TABLE_NAME) @@ -347,7 +350,7 @@ class ChatItemArchiveImporter( if (contact != null) { val contactAttachment: Attachment? = contact.avatarAttachment - followUp = { messageRowId -> + followUps += { messageRowId -> val attachmentMap = if (contactAttachment != null) { SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, listOf(contactAttachment), emptyList()) } else { @@ -365,6 +368,19 @@ class ChatItemArchiveImporter( } } + if (this.directStoryReplyMessage != null) { + val longTextAttachment: Attachment? = this.directStoryReplyMessage.textReply?.longText?.toLocalAttachment( + importState = importState, + contentType = "text/x-signal-plain" + ) + + if (longTextAttachment != null) { + followUps += { messageRowId -> + SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, listOf(longTextAttachment), emptyList()) + } + } + } + if (this.standardMessage != null) { val bodyRanges = this.standardMessage.text?.bodyRanges if (!bodyRanges.isNullOrEmpty()) { @@ -380,7 +396,7 @@ class ChatItemArchiveImporter( } } if (mentions.isNotEmpty()) { - followUp = { messageId -> + followUps += { messageId -> SignalDatabase.mentions.insert(threadId, messageId, mentions) } } @@ -391,19 +407,17 @@ class ChatItemArchiveImporter( attachment.toLocalAttachment() } - val longTextAttachments: List = this.standardMessage.longText?.let { longTextPointer -> - longTextPointer.toLocalAttachment( - importState = importState, - contentType = "text/x-signal-plain" - ) - }?.let { listOf(it) } ?: emptyList() + val longTextAttachments: List = this.standardMessage.longText?.toLocalAttachment( + importState = importState, + contentType = "text/x-signal-plain" + )?.let { listOf(it) } ?: emptyList() val quoteAttachments: List = this.standardMessage.quote?.toLocalAttachments() ?: emptyList() val hasAttachments = attachments.isNotEmpty() || linkPreviewAttachments.isNotEmpty() || quoteAttachments.isNotEmpty() || longTextAttachments.isNotEmpty() if (hasAttachments || linkPreviews.isNotEmpty()) { - followUp = { messageRowId -> + followUps += { messageRowId -> val attachmentMap = if (hasAttachments) { SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, attachments + linkPreviewAttachments + longTextAttachments, quoteAttachments) } else { @@ -424,7 +438,7 @@ class ChatItemArchiveImporter( val sticker = this.stickerMessage.sticker val attachment = sticker.toLocalAttachment() if (attachment != null) { - followUp = { messageRowId -> + followUps += { messageRowId -> SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, listOf(attachment), emptyList()) } } @@ -433,12 +447,20 @@ class ChatItemArchiveImporter( if (this.viewOnceMessage != null) { val attachment = this.viewOnceMessage.attachment?.toLocalAttachment() if (attachment != null) { - followUp = { messageRowId -> + followUps += { messageRowId -> SignalDatabase.attachments.insertAttachmentsForMessage(messageRowId, listOf(attachment), emptyList()) } } } + val followUp: ((Long) -> Unit)? = if (followUps.isNotEmpty()) { + { messageId -> + followUps.forEach { it(messageId) } + } + } else { + null + } + return MessageInsert(contentValues, followUp) } @@ -505,6 +527,7 @@ class ChatItemArchiveImporter( this.paymentNotification != null -> contentValues.addPaymentNotification(this, chatRecipientId) this.giftBadge != null -> contentValues.addGiftBadge(this.giftBadge) this.viewOnceMessage != null -> contentValues.addViewOnce(this.viewOnceMessage) + this.directStoryReplyMessage != null -> contentValues.addDirectStoryReply(this.directStoryReplyMessage) } return contentValues @@ -548,6 +571,7 @@ class ChatItemArchiveImporter( this.contactMessage != null -> this.contactMessage.reactions this.stickerMessage != null -> this.stickerMessage.reactions this.viewOnceMessage != null -> this.viewOnceMessage.reactions + this.directStoryReplyMessage != null -> this.directStoryReplyMessage.reactions else -> emptyList() } @@ -625,6 +649,10 @@ class ChatItemArchiveImporter( type = type or MessageTypes.SPECIAL_TYPE_GIFT_BADGE } + if (this.directStoryReplyMessage?.emoji != null) { + type = type or MessageTypes.SPECIAL_TYPE_STORY_REACTION + } + return type } @@ -848,6 +876,19 @@ class ChatItemArchiveImporter( put(MessageTable.VIEW_ONCE, true.toInt()) } + private fun ContentValues.addDirectStoryReply(directStoryReply: DirectStoryReplyMessage) { + put(MessageTable.PARENT_STORY_ID, directStoryReply.storySentTimestamp?.takeUnless { it == 0L } ?: MessageTable.PARENT_STORY_MISSING_ID) + + if (directStoryReply.emoji != null) { + put(MessageTable.BODY, directStoryReply.emoji) + } + + if (directStoryReply.textReply != null) { + put(MessageTable.BODY, directStoryReply.textReply.text?.body) + put(MessageTable.MESSAGE_RANGES, directStoryReply.textReply.text?.bodyRanges?.toLocalBodyRanges()?.encode()) + } + } + private fun String?.tryParseMoney(): Money? { if (this.isNullOrEmpty()) { return null diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index 820e3b81f7..6ccdd3ab64 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -210,6 +210,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat const val QUOTE_TARGET_MISSING_ID = -1L const val ADDRESSABLE_MESSAGE_LIMIT = 5 + const val PARENT_STORY_MISSING_ID = -1L const val CREATE_TABLE = """ CREATE TABLE $TABLE_NAME (