From 98865d61dd39c548de249f46036869b6628c64ae Mon Sep 17 00:00:00 2001 From: Clark Date: Wed, 24 Jan 2024 15:53:52 -0500 Subject: [PATCH] Convert gv2 update messages to backup distinct protos. --- .../test/ConversationElementGenerator.kt | 3 +- .../ConversationListDataSource.java | 7 +- .../ConversationListItem.java | 6 +- .../securesms/database/MessageTable.kt | 13 +- .../securesms/database/ThreadTable.kt | 20 +- .../helpers/SignalDatabaseMigrations.kt | 6 +- .../V217_MessageTableExtrasColumn.kt | 21 + .../model/GroupsV2UpdateMessageProducer.java | 588 ++++++++++++++++++ .../database/model/InMemoryMessageRecord.java | 3 +- .../database/model/MessageRecord.java | 36 +- .../database/model/MmsMessageRecord.java | 16 +- .../database/model/ThreadRecord.java | 93 +-- app/src/main/protowire/Backup.proto | 274 +++++++- app/src/main/protowire/Database.proto | 11 + .../securesms/database/FakeMessageRecords.kt | 3 +- 15 files changed, 1019 insertions(+), 81 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V217_MessageTableExtrasColumn.kt diff --git a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/ConversationElementGenerator.kt b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/ConversationElementGenerator.kt index 2a0b90f4ce..1b358b84cd 100644 --- a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/ConversationElementGenerator.kt +++ b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/ConversationElementGenerator.kt @@ -118,7 +118,8 @@ class ConversationElementGenerator { null, null, 0, - false + false, + null ) val conversationMessage = ConversationMessageFactory.createWithUnresolvedData( diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java index beffad736e..12d0e9cbc6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListDataSource.java @@ -84,7 +84,12 @@ abstract class ConversationListDataSource implements PagedDataSource = MMS_PROJECTION_BASE + "NULL AS ${AttachmentTable.ATTACHMENT_JSON_ALIAS}" @@ -4985,6 +4989,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat val originalMessageId: MessageId? = cursor.requireLong(ORIGINAL_MESSAGE_ID).let { if (it == 0L) null else MessageId(it) } val editCount = cursor.requireInt(REVISION_NUMBER) val isRead = cursor.requireBoolean(READ) + val messageExtraBytes = cursor.requireBlob(MESSAGE_EXTRAS) + val messageExtras = if (messageExtraBytes != null) MessageExtras.ADAPTER.decode(messageExtraBytes) else null if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { hasReadReceipt = false @@ -5072,7 +5078,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat latestRevisionId, originalMessageId, editCount, - isRead + isRead, + messageExtras ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt index 5fd13a7370..7503e1876f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt @@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList +import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras import org.thoughtcrime.securesms.database.model.serialize import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.groups.BadGroupIdException @@ -97,6 +98,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa const val SNIPPET_URI = "snippet_uri" const val SNIPPET_CONTENT_TYPE = "snippet_content_type" const val SNIPPET_EXTRAS = "snippet_extras" + const val SNIPPET_MESSAGE_EXTRAS = "snippet_message_extras" const val ARCHIVED = "archived" const val STATUS = "status" const val HAS_DELIVERY_RECEIPT = "has_delivery_receipt" @@ -137,7 +139,8 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa $LAST_SCROLLED INTEGER DEFAULT 0, $PINNED INTEGER DEFAULT 0, $UNREAD_SELF_MENTION_COUNT INTEGER DEFAULT 0, - $ACTIVE INTEGER DEFAULT 0 + $ACTIVE INTEGER DEFAULT 0, + $SNIPPET_MESSAGE_EXTRAS BLOB DEFAULT NULL ) """ @@ -164,6 +167,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa SNIPPET_URI, SNIPPET_CONTENT_TYPE, SNIPPET_EXTRAS, + SNIPPET_MESSAGE_EXTRAS, ARCHIVED, STATUS, HAS_DELIVERY_RECEIPT, @@ -223,7 +227,8 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa expiresIn: Long, readReceiptCount: Int, unreadCount: Int, - unreadMentionCount: Int + unreadMentionCount: Int, + messageExtras: MessageExtras? ) { var extraSerialized: String? = null @@ -249,7 +254,8 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa EXPIRES_IN to expiresIn, ACTIVE to 1, UNREAD_COUNT to unreadCount, - UNREAD_SELF_MENTION_COUNT to unreadMentionCount + UNREAD_SELF_MENTION_COUNT to unreadMentionCount, + SNIPPET_MESSAGE_EXTRAS to messageExtras?.encode() ) writableDatabase @@ -1479,7 +1485,8 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa expiresIn = 0, readReceiptCount = 0, unreadCount = 0, - unreadMentionCount = 0 + unreadMentionCount = 0, + messageExtras = null ) } return@withinTransaction true @@ -1508,7 +1515,8 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa expiresIn = record.expiresIn, readReceiptCount = record.hasReadReceipt().toInt(), unreadCount = unreadCount, - unreadMentionCount = unreadMentionCount + unreadMentionCount = unreadMentionCount, + messageExtras = record.messageExtras ) if (notifyListeners) { @@ -1667,6 +1675,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa SNIPPET_URI to null, SNIPPET_CONTENT_TYPE to null, SNIPPET_EXTRAS to null, + SNIPPET_MESSAGE_EXTRAS to null, UNREAD_COUNT to 0, ARCHIVED to 0, STATUS to 0, @@ -1898,6 +1907,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa val hasReadReceipt = TextSecurePreferences.isReadReceiptsEnabled(context) && cursor.requireBoolean(HAS_READ_RECEIPT) val extraString = cursor.getString(cursor.getColumnIndexOrThrow(SNIPPET_EXTRAS)) + val messageExtras = cursor.getBlob(cursor.getColumnIndexOrThrow(SNIPPET_MESSAGE_EXTRAS)) val extra: Extra? = if (extraString != null) { try { val jsonObject = SaneJSONObject(JSONObject(extraString)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index cc98a4818c..086c5308ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -74,6 +74,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V213_FixUsernameInE import org.thoughtcrime.securesms.database.helpers.migration.V214_PhoneNumberSharingColumn import org.thoughtcrime.securesms.database.helpers.migration.V215_RemoveAttachmentUniqueId import org.thoughtcrime.securesms.database.helpers.migration.V216_PhoneNumberDiscoverable +import org.thoughtcrime.securesms.database.helpers.migration.V217_MessageTableExtrasColumn /** * Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness. @@ -150,10 +151,11 @@ object SignalDatabaseMigrations { 213 to V213_FixUsernameInE164Column, 214 to V214_PhoneNumberSharingColumn, 215 to V215_RemoveAttachmentUniqueId, - 216 to V216_PhoneNumberDiscoverable + 216 to V216_PhoneNumberDiscoverable, + 217 to V217_MessageTableExtrasColumn ) - const val DATABASE_VERSION = 216 + const val DATABASE_VERSION = 217 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V217_MessageTableExtrasColumn.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V217_MessageTableExtrasColumn.kt new file mode 100644 index 0000000000..a8a24d2bd2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/migration/V217_MessageTableExtrasColumn.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.database.helpers.migration + +import android.app.Application +import net.zetetic.database.sqlcipher.SQLiteDatabase + +/** + * Adds a message_extras column to the messages table. This allows us to + * store extra data for messages in a more future proof and structured way. + */ +@Suppress("ClassName") +object V217_MessageTableExtrasColumn : SignalDatabaseMigration { + override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { + db.execSQL("ALTER TABLE message ADD COLUMN message_extras BLOB DEFAULT NULL") + db.execSQL("ALTER TABLE thread ADD COLUMN snippet_message_extras BLOB DEFAULT NULL") + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java index 05103794cc..bb0fe2d825 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/GroupsV2UpdateMessageProducer.java @@ -25,17 +25,53 @@ import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemov import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember; import org.signal.storageservice.protos.groups.local.EnabledState; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.backup.v2.proto.GenericGroupUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupAdminStatusUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupAnnouncementOnlyChangeUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupAttributesAccessLevelChangeUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupAvatarUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupChangeChatUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupCreationUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupDescriptionUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupInvitationAcceptedUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupInvitationDeclinedUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupInvitationRevokedUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupInviteLinkAdminApprovalUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupInviteLinkDisabledUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupInviteLinkEnabledUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupInviteLinkResetUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupJoinRequestApprovalUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupJoinRequestCanceledUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupJoinRequestUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberAddedUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberJoinedUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberLeftUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupMemberRemovedUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupMembershipAccessLevelChangeUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupNameUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupUnknownInviteeUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupV2AccessLevel; +import org.thoughtcrime.securesms.backup.v2.proto.GroupV2MigrationDroppedMembersUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupV2MigrationInvitedMembersUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupV2MigrationSelfInvitedUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.GroupV2MigrationUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.SelfInvitedOtherUserToGroupUpdate; +import org.thoughtcrime.securesms.backup.v2.proto.SelfInvitedToGroupUpdate; +import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription; import org.thoughtcrime.securesms.groups.GV2AccessLevelUtil; +import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.util.ExpirationUtil; import org.thoughtcrime.securesms.util.SpanUtil; +import org.thoughtcrime.securesms.util.Util; import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupUtil; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.ServiceId.ACI; import org.whispersystems.signalservice.api.push.ServiceIds; import org.whispersystems.signalservice.api.util.UuidUtil; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -104,6 +140,485 @@ final class GroupsV2UpdateMessageProducer { } } + List describeChanges(@NonNull List groupUpdates) { + List updates = new LinkedList<>(); + for (GroupChangeChatUpdate.Update update : groupUpdates) { + describeUpdate(update, updates); + } + + return updates; + } + + private void describeUpdate(@NonNull GroupChangeChatUpdate.Update update, @NonNull List updates) { + if (update.genericGroupUpdate != null) { + describeGenericGroupUpdate(update.genericGroupUpdate, updates); + } else if (update.groupCreationUpdate != null) { + describeGroupCreationUpdate(update.groupCreationUpdate, updates); + } else if (update.groupNameUpdate != null) { + describeGroupNameUpdate(update.groupNameUpdate, updates); + } else if (update.groupAvatarUpdate != null) { + describeAvatarChange(update.groupAvatarUpdate, updates); + } else if (update.groupDescriptionUpdate != null) { + describeDescriptionChange(update.groupDescriptionUpdate, updates); + } else if (update.groupMembershipAccessLevelChangeUpdate != null) { + describeGroupMembershipAccessLevelChange(update.groupMembershipAccessLevelChangeUpdate, updates); + } else if (update.groupAttributesAccessLevelChangeUpdate != null) { + describeGroupAttributesAccessLevelChange(update.groupAttributesAccessLevelChangeUpdate, updates); + } else if (update.groupAnnouncementOnlyChangeUpdate != null) { + describeGroupAnnouncementOnlyUpdate(update.groupAnnouncementOnlyChangeUpdate, updates); + } else if (update.groupAdminStatusUpdate != null) { + describeAdminStatusChange(update.groupAdminStatusUpdate, updates); + } else if (update.groupMemberLeftUpdate != null) { + describeGroupMemberLeftChange(update.groupMemberLeftUpdate, updates); + } else if (update.groupMemberRemovedUpdate != null) { + describeGroupMemberRemovedChange(update.groupMemberRemovedUpdate, updates); + } else if (update.selfInvitedToGroupUpdate != null) { + describeSelfInvitedToGroupUpdate(update.selfInvitedToGroupUpdate, updates); + } else if (update.selfInvitedOtherUserToGroupUpdate != null) { + describeSelfInvitedOtherUserToGroupUpdate(update.selfInvitedOtherUserToGroupUpdate, updates); + } else if (update.groupUnknownInviteeUpdate != null) { + describeUnknownUsersInvitedUpdate(update.groupUnknownInviteeUpdate, updates); + } else if (update.groupInvitationAcceptedUpdate != null) { + describeGroupInvitationAcceptedUpdate(update.groupInvitationAcceptedUpdate, updates); + } else if (update.groupMemberJoinedUpdate != null) { + describeGroupMemberJoinedUpdate(update.groupMemberJoinedUpdate, updates); + } else if (update.groupMemberAddedUpdate != null) { + describeGroupMemberAddedUpdate(update.groupMemberAddedUpdate, updates); + } else if (update.groupInvitationDeclinedUpdate != null) { + describeGroupInvitationDeclinedUpdate(update.groupInvitationDeclinedUpdate, updates); + } else if (update.groupInvitationRevokedUpdate != null) { + describeGroupInvitationRevokedUpdate(update.groupInvitationRevokedUpdate, updates); + } else if (update.groupJoinRequestUpdate != null) { + describeGroupJoinRequestUpdate(update.groupJoinRequestUpdate, updates); + } else if (update.groupJoinRequestApprovalUpdate != null) { + describeGroupJoinRequestApprovedUpdate(update.groupJoinRequestApprovalUpdate, updates); + } else if (update.groupJoinRequestCanceledUpdate != null) { + describeGroupJoinRequestCanceledUpdate(update.groupJoinRequestCanceledUpdate, updates); + } else if (update.groupInviteLinkResetUpdate != null) { + describeInviteLinkResetUpdate(update.groupInviteLinkResetUpdate, updates); + } else if (update.groupInviteLinkEnabledUpdate != null) { + describeInviteLinkEnabledUpdate(update.groupInviteLinkEnabledUpdate, updates); + } else if (update.groupInviteLinkDisabledUpdate != null) { + describeInviteLinkDisabledUpdate(update.groupInviteLinkDisabledUpdate, updates); + } else if (update.groupInviteLinkAdminApprovalUpdate != null) { + describeGroupInviteLinkAdminApprovalUpdate(update.groupInviteLinkAdminApprovalUpdate, updates); + } else if (update.groupV2MigrationUpdate != null) { + describeGroupV2MigrationUpdate(update.groupV2MigrationUpdate, updates); + } else if (update.groupV2MigrationDroppedMembersUpdate != null) { + describeGroupV2MigrationDroppedMembersUpdate(update.groupV2MigrationDroppedMembersUpdate, updates); + } else if (update.groupV2MigrationInvitedMembersUpdate != null) { + describeGroupV2MigrationInvitedMembersUpdate(update.groupV2MigrationInvitedMembersUpdate, updates); + } else if (update.groupV2MigrationSelfInvitedUpdate != null) { + describeGroupV2MigrationSelfInvitedUpdate(update.groupV2MigrationSelfInvitedUpdate, updates); + } + } + + private void describeGroupV2MigrationSelfInvitedUpdate(@NonNull GroupV2MigrationSelfInvitedUpdate update, @NonNull List updates) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_couldnt_be_added_to_the_new_group_and_have_been_invited_to_join), R.drawable.ic_update_group_add_16)); + } + + private void describeGroupV2MigrationDroppedMembersUpdate(@NonNull GroupV2MigrationDroppedMembersUpdate update, @NonNull List updates) { + updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_members_couldnt_be_added_to_the_new_group_and_have_been_removed, update.droppedMembersCount, update.droppedMembersCount), R.drawable.ic_update_group_remove_16)); + } + + private void describeGroupV2MigrationInvitedMembersUpdate(@NonNull GroupV2MigrationInvitedMembersUpdate update, @NonNull List updates) { + updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_members_couldnt_be_added_to_the_new_group_and_have_been_invited, update.invitedMembersCount, update.invitedMembersCount), R.drawable.ic_update_group_remove_16)); + } + + private void describeGroupV2MigrationUpdate(@NonNull GroupV2MigrationUpdate update, @NonNull List updates) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_this_group_was_updated_to_a_new_group), R.drawable.ic_update_group_role_16)); + } + + private void describeGroupInviteLinkAdminApprovalUpdate(@NonNull GroupInviteLinkAdminApprovalUpdate update, @NonNull List updates) { + if (update.updaterAci == null) { + if (update.linkRequiresAdminApproval) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_the_admin_approval_for_the_group_link_has_been_turned_on), R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(context.getString(R.string.MessageRecord_the_admin_approval_for_the_group_link_has_been_turned_off), R.drawable.ic_update_group_role_16)); + } + } else { + if (selfIds.matches(update.updaterAci)) { + if (update.linkRequiresAdminApproval) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_admin_approval_for_the_group_link), R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_admin_approval_for_the_group_link), R.drawable.ic_update_group_role_16)); + } + } else { + if (update.linkRequiresAdminApproval) { + updates.add(updateDescription(R.string.MessageRecord_s_turned_on_admin_approval_for_the_group_link, update.updaterAci, R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_turned_off_admin_approval_for_the_group_link, update.updaterAci, R.drawable.ic_update_group_role_16)); + } + } + } + } + + private void describeInviteLinkDisabledUpdate(@NonNull GroupInviteLinkDisabledUpdate update, @NonNull List updates) { + boolean editorIsYou = selfIds.matches(update.updaterAci); + + if (update.updaterAci == null) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_off), R.drawable.ic_update_group_role_16)); + } else { + if (editorIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_off_the_group_link), R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_turned_off_the_group_link, update.updaterAci, R.drawable.ic_update_group_role_16)); + } + } + } + + private void describeInviteLinkEnabledUpdate(@NonNull GroupInviteLinkEnabledUpdate update, @NonNull List updates) { + boolean editorIsYou = selfIds.matches(update.updaterAci); + + if (update.updaterAci == null) { + if (update.linkRequiresAdminApproval) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_on), R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_turned_on_with_admin_approval_off), R.drawable.ic_update_group_role_16)); + } + } else { + if (editorIsYou) { + if (update.linkRequiresAdminApproval) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_on), R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_turned_on_the_group_link_with_admin_approval_off), R.drawable.ic_update_group_role_16)); + } + } else { + if (update.linkRequiresAdminApproval) { + updates.add(updateDescription(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_on, update.updaterAci, R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_turned_on_the_group_link_with_admin_approval_off, update.updaterAci, R.drawable.ic_update_group_role_16)); + } + } + } + } + + private void describeInviteLinkResetUpdate(@NonNull GroupInviteLinkResetUpdate update, @NonNull List updates) { + if (update.updaterAci == null) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_link_has_been_reset), R.drawable.ic_update_group_role_16)); + } else { + if (selfIds.matches(update.updaterAci)) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_reset_the_group_link), R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_reset_the_group_link, update.updaterAci, R.drawable.ic_update_group_role_16)); + } + } + } + + private void describeGroupJoinRequestCanceledUpdate(@NonNull GroupJoinRequestCanceledUpdate update, @NonNull List updates) { + boolean requestingMemberIsYou = selfIds.matches(update.requestorAci); + + if (requestingMemberIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_canceled_your_request_to_join_the_group), R.drawable.ic_update_group_decline_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_canceled_their_request_to_join_the_group, update.requestorAci, R.drawable.ic_update_group_decline_16)); + } + } + + private void describeGroupJoinRequestApprovedUpdate(@NonNull GroupJoinRequestApprovalUpdate update, @NonNull List updates) { + boolean requestingMemberIsYou = selfIds.matches(update.requestorAci); + + if (update.wasApproved) { + if (update.updaterAci == null) { + if (requestingMemberIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_approved), R.drawable.ic_update_group_accept_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_approved, update.requestorAci, R.drawable.ic_update_group_accept_16)); + } + } else { + if (requestingMemberIsYou) { + updates.add(updateDescription(R.string.MessageRecord_s_approved_your_request_to_join_the_group, update.updaterAci, R.drawable.ic_update_group_accept_16)); + } else { + boolean editorIsYou = selfIds.matches(update.updaterAci); + + if (editorIsYou) { + updates.add(updateDescription(R.string.MessageRecord_you_approved_a_request_to_join_the_group_from_s, update.requestorAci, R.drawable.ic_update_group_accept_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_approved_a_request_to_join_the_group_from_s, update.updaterAci, update.requestorAci, R.drawable.ic_update_group_accept_16)); + } + } + } + } else { + if (update.updaterAci == null) { + if (requestingMemberIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin), R.drawable.ic_update_group_decline_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_a_request_to_join_the_group_from_s_has_been_denied, update.requestorAci, R.drawable.ic_update_group_decline_16)); + } + } else { + if (requestingMemberIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_your_request_to_join_the_group_has_been_denied_by_an_admin), R.drawable.ic_update_group_decline_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_denied_a_request_to_join_the_group_from_s, update.updaterAci, update.requestorAci, R.drawable.ic_update_group_decline_16)); + } + } + } + } + + private void describeGroupJoinRequestUpdate(@NonNull GroupJoinRequestUpdate update, @NonNull List updates) { + if (selfIds.matches(update.requestorAci)) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_sent_a_request_to_join_the_group), R.drawable.ic_update_group_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_requested_to_join_via_the_group_link, update.requestorAci, R.drawable.ic_update_group_16)); + } + } + + private void describeGroupInvitationRevokedUpdate(@NonNull GroupInvitationRevokedUpdate update, @NonNull List updates) { + int revokedMeCount = 0; + for (GroupInvitationRevokedUpdate.Invitee invitee : update.invitees) { + if (selfIds.matches(invitee.inviteeAci) || selfIds.matches(invitee.inviteePni)) { + revokedMeCount++; + } + } + + int notMeInvitees = update.invitees.size() - revokedMeCount; + + if (update.updaterAci == null) { + if (revokedMeCount > 0) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_an_admin_revoked_your_invitation_to_the_group), R.drawable.ic_update_group_decline_16)); + } + if (notMeInvitees > 0) { + updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_invitations_were_revoked, notMeInvitees, notMeInvitees), R.drawable.ic_update_group_decline_16)); + } + } else { + if (revokedMeCount > 0) { + updates.add(updateDescription(R.string.MessageRecord_s_revoked_your_invitation_to_the_group, update.updaterAci, R.drawable.ic_update_group_decline_16)); + } + if (selfIds.matches(update.updaterAci)) { + updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_you_revoked_invites, notMeInvitees, notMeInvitees), R.drawable.ic_update_group_decline_16)); + } else { + updates.add(updateDescription(R.plurals.MessageRecord_s_revoked_invites, notMeInvitees, update.updaterAci, notMeInvitees, R.drawable.ic_update_group_decline_16)); + } + } + } + + private void describeGroupInvitationDeclinedUpdate(@NonNull GroupInvitationDeclinedUpdate update, @NonNull List updates) { + if (selfIds.matches(update.inviteeAci)) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_declined_the_invitation_to_the_group), R.drawable.ic_update_group_decline_16)); + } else { + updates.add(updateDescription(context.getString(R.string.MessageRecord_someone_declined_an_invitation_to_the_group), R.drawable.ic_update_group_decline_16)); + } + } + + private void describeGroupMemberAddedUpdate(@NonNull GroupMemberAddedUpdate update, @NonNull List updates) { + boolean newMemberIsYou = selfIds.matches(update.newMemberAci); + + if (update.updaterAci == null) { + if (newMemberIsYou) { + updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_joined_the_group, update.newMemberAci, R.drawable.ic_update_group_add_16)); + } + } else if (update.hadOpenInvitation) { + if (selfIds.matches(update.updaterAci)) { + updates.add(updateDescription(R.string.MessageRecord_you_added_invited_member_s, update.newMemberAci, R.drawable.ic_update_group_add_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_added_invited_member_s, update.updaterAci, update.newMemberAci, R.drawable.ic_update_group_add_16)); + } + } else { + if (newMemberIsYou) { + updates.add(0, updateDescription(R.string.MessageRecord_s_added_you, update.updaterAci, R.drawable.ic_update_group_add_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_added_s, update.updaterAci, update.newMemberAci, R.drawable.ic_update_group_add_16)); + } + } + } + + private void describeGroupMemberJoinedUpdate(@NonNull GroupMemberJoinedUpdate update, @NonNull List updates) { + boolean newMemberIsYou = selfIds.matches(update.newMemberAci); + + if (newMemberIsYou) { + updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_joined_the_group), R.drawable.ic_update_group_add_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_joined_the_group, update.newMemberAci, R.drawable.ic_update_group_add_16)); + } + } + + private void describeGroupInvitationAcceptedUpdate(@NonNull GroupInvitationAcceptedUpdate update, @NonNull List updates) { + if (selfIds.matches(update.newMemberAci)) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_accepted_invite), R.drawable.ic_update_group_accept_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_accepted_invite, update.newMemberAci, R.drawable.ic_update_group_accept_16)); + } + } + + private void describeUnknownUsersInvitedUpdate(@NonNull GroupUnknownInviteeUpdate update, @NonNull List updates) { + if (update.inviterAci == null) { + updates.add(updateDescription(context.getResources().getQuantityString(R.plurals.MessageRecord_d_people_were_invited_to_the_group, update.inviteeCount, update.inviteeCount), R.drawable.ic_update_group_add_16)); + } else { + updates.add(updateDescription(R.plurals.MessageRecord_s_invited_members, update.inviteeCount, update.inviterAci, update.inviteeCount, R.drawable.ic_update_group_add_16)); + } + } + private void describeSelfInvitedOtherUserToGroupUpdate(@NonNull SelfInvitedOtherUserToGroupUpdate update, @NonNull List updates) { + updates.add(updateDescription(R.string.MessageRecord_you_invited_s_to_the_group, update.inviteeServiceId, R.drawable.ic_update_group_add_16)); + } + + private void describeSelfInvitedToGroupUpdate(@NonNull SelfInvitedToGroupUpdate update, @NonNull List updates) { + if (update.inviterAci == null) { + updates.add(0, updateDescription(context.getString(R.string.MessageRecord_you_were_invited_to_the_group), R.drawable.ic_update_group_add_16)); + } else { + updates.add(0, updateDescription(R.string.MessageRecord_s_invited_you_to_the_group, update.inviterAci, R.drawable.ic_update_group_add_16)); + } + } + + private void describeGenericGroupUpdate(@NonNull GenericGroupUpdate update, @NonNull List updates) { + if (update.updaterAci == null) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_was_updated), R.drawable.ic_update_group_16)); + } else { + boolean editorIsYou = selfIds.matches(update.updaterAci); + + if (editorIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_updated_group), R.drawable.ic_update_group_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_updated_group, update.updaterAci, R.drawable.ic_update_group_16)); + } + } + } + + private void describeGroupCreationUpdate(@NonNull GroupCreationUpdate update, @NonNull List updates) { + if (update.updaterAci == null) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_group_updated), R.drawable.ic_update_group_16)); + } else { + if (selfIds.matches(update.updaterAci)) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_created_the_group), R.drawable.ic_update_group_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_added_you, update.updaterAci, R.drawable.ic_update_group_add_16)); + } + } + } + + private void describeGroupNameUpdate(@NonNull GroupNameUpdate update, @NonNull List updates) { + if (update.updaterAci == null) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, StringUtil.isolateBidi(update.newGroupName)), R.drawable.ic_update_group_name_16)); + } else { + String newTitle = StringUtil.isolateBidi(update.newGroupName); + if (selfIds.matches(update.updaterAci)) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_name_to_s, newTitle), R.drawable.ic_update_group_name_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_changed_the_group_name_to_s, update.updaterAci, newTitle, R.drawable.ic_update_group_name_16)); + } + } + } + + private void describeGroupMembershipAccessLevelChange(@NonNull GroupMembershipAccessLevelChangeUpdate update, @NonNull List updates) { + if (update.accessLevel == GroupV2AccessLevel.UNKNOWN) { + return; + } + String accessLevel = GV2AccessLevelUtil.toString(context, backupGv2AccessLevelToGroups(update.accessLevel)); + if (update.updaterAci == null) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_membership_has_been_changed_to_s, accessLevel), R.drawable.ic_update_group_role_16)); + } else { + boolean editorIsYou = selfIds.matches(update.updaterAci); + + if (editorIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_membership_to_s, accessLevel), R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_changed_who_can_edit_group_membership_to_s, update.updaterAci, accessLevel, R.drawable.ic_update_group_role_16)); + } + } + } + + private void describeGroupAttributesAccessLevelChange(@NonNull GroupAttributesAccessLevelChangeUpdate update, @NonNull List updates) { + if (update.accessLevel == GroupV2AccessLevel.UNKNOWN) { + return; + } + String accessLevel = GV2AccessLevelUtil.toString(context, backupGv2AccessLevelToGroups(update.accessLevel)); + if (update.updaterAci == null) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_who_can_edit_group_info_has_been_changed_to_s, accessLevel), R.drawable.ic_update_group_role_16)); + } else { + boolean editorIsYou = selfIds.matches(update.updaterAci); + + if (editorIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_who_can_edit_group_info_to_s, accessLevel), R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_changed_who_can_edit_group_info_to_s, update.updaterAci, accessLevel, R.drawable.ic_update_group_role_16)); + } + } + } + + private void describeGroupAnnouncementOnlyUpdate(@NonNull GroupAnnouncementOnlyChangeUpdate update, @NonNull List updates) { + if (update.updaterAci == null) { + if (update.isAnnouncementOnly) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_allow_only_admins_to_send), R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(context.getString(R.string.MessageRecord_allow_all_members_to_send), R.drawable.ic_update_group_role_16)); + } + } else { + boolean editorIsYou = selfIds.matches(update.updaterAci); + + if (update.isAnnouncementOnly) { + if (editorIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_allow_only_admins_to_send), R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_allow_only_admins_to_send, update.updaterAci, R.drawable.ic_update_group_role_16)); + } + } else { + if (editorIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_allow_all_members_to_send), R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_allow_all_members_to_send, update.updaterAci, R.drawable.ic_update_group_role_16)); + } + } + } + } + + private void describeGroupMemberLeftChange(@NonNull GroupMemberLeftUpdate update, @NonNull List updates) { + if (update.aci == null) { + return; + } + boolean editorIsYou = selfIds.matches(update.aci); + if (editorIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_left_the_group), R.drawable.ic_update_group_leave_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_left_the_group, update.aci, R.drawable.ic_update_group_leave_16)); + } + } + + private void describeGroupMemberRemovedChange(@NonNull GroupMemberRemovedUpdate update, @NonNull List updates) { + if (update.removerAci == null) { + boolean removedMemberIsYou = selfIds.matches(update.removedAci); + + if (removedMemberIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_in_the_group), R.drawable.ic_update_group_leave_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_is_no_longer_in_the_group, update.removedAci, R.drawable.ic_update_group_leave_16)); + } + } else { + boolean editorIsYou = selfIds.matches(update.removerAci); + + boolean removedMemberIsYou = selfIds.matches(update.removedAci); + + if (editorIsYou) { + if (removedMemberIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_left_the_group), R.drawable.ic_update_group_leave_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_you_removed_s, update.removedAci, R.drawable.ic_update_group_remove_16)); + } + } else { + if (removedMemberIsYou) { + updates.add(updateDescription(R.string.MessageRecord_s_removed_you_from_the_group, update.removerAci, R.drawable.ic_update_group_remove_16)); + } else { + if (update.removerAci.equals(update.removedAci)) { + updates.add(updateDescription(R.string.MessageRecord_s_left_the_group, update.removedAci, R.drawable.ic_update_group_leave_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_removed_s, update.removerAci, update.removedAci, R.drawable.ic_update_group_remove_16)); + } + } + } + } + } + + private AccessControl.AccessRequired backupGv2AccessLevelToGroups(@NonNull GroupV2AccessLevel accessLevel) { + switch (accessLevel) { + case ANY: return AccessControl.AccessRequired.ANY; + case MEMBER: return AccessControl.AccessRequired.MEMBER; + case ADMINISTRATOR: return AccessControl.AccessRequired.ADMINISTRATOR; + case UNSATISFIABLE: return AccessControl.AccessRequired.UNSATISFIABLE; + default: + case UNKNOWN: return AccessControl.AccessRequired.UNKNOWN; + } + } + List describeChanges(@Nullable DecryptedGroup previousGroupState, @NonNull DecryptedGroupChange change) { if (new DecryptedGroup().equals(previousGroupState)) { previousGroupState = null; @@ -317,6 +832,51 @@ final class GroupsV2UpdateMessageProducer { } } + private void describeAdminStatusChange(@NonNull GroupAdminStatusUpdate groupAdminStatusUpdate, List updates) { + boolean changedMemberIsYou = selfIds.matches(groupAdminStatusUpdate.memberAci); + + if (groupAdminStatusUpdate.updaterAci != null) { + boolean editorIsYou = selfIds.matches(groupAdminStatusUpdate.updaterAci); + + if (groupAdminStatusUpdate.wasAdminStatusGranted) { + if (editorIsYou) { + updates.add(updateDescription(R.string.MessageRecord_you_made_s_an_admin, groupAdminStatusUpdate.memberAci, R.drawable.ic_update_group_role_16)); + } else { + if (changedMemberIsYou) { + updates.add(updateDescription(R.string.MessageRecord_s_made_you_an_admin, groupAdminStatusUpdate.updaterAci, R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_made_s_an_admin, groupAdminStatusUpdate.updaterAci, groupAdminStatusUpdate.memberAci, R.drawable.ic_update_group_role_16)); + + } + } + } else { + if (editorIsYou) { + updates.add(updateDescription(R.string.MessageRecord_you_revoked_admin_privileges_from_s, groupAdminStatusUpdate.memberAci, R.drawable.ic_update_group_role_16)); + } else { + if (changedMemberIsYou) { + updates.add(updateDescription(R.string.MessageRecord_s_revoked_your_admin_privileges, groupAdminStatusUpdate.updaterAci, R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_revoked_admin_privileges_from_s, groupAdminStatusUpdate.updaterAci, groupAdminStatusUpdate.memberAci, R.drawable.ic_update_group_role_16)); + } + } + } + } else { + if (groupAdminStatusUpdate.wasAdminStatusGranted) { + if (changedMemberIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_now_an_admin), R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_is_now_an_admin, groupAdminStatusUpdate.memberAci, R.drawable.ic_update_group_role_16)); + } + } else { + if (changedMemberIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_are_no_longer_an_admin), R.drawable.ic_update_group_role_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_is_no_longer_an_admin, groupAdminStatusUpdate.memberAci, R.drawable.ic_update_group_role_16)); + } + } + } + } + private void describeInvitations(@NonNull DecryptedGroupChange change, @NonNull List updates) { boolean editorIsYou = selfIds.matches(change.editorServiceIdBytes); int notYouInviteCount = 0; @@ -481,6 +1041,20 @@ final class GroupsV2UpdateMessageProducer { } } + private void describeDescriptionChange(@NonNull GroupDescriptionUpdate groupDescriptionUpdate, @NonNull List updates) { + if (groupDescriptionUpdate.updaterAci != null) { + boolean editorIsYou = selfIds.matches(groupDescriptionUpdate.updaterAci); + + if (editorIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_description), R.drawable.ic_update_group_name_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_changed_the_group_description, groupDescriptionUpdate.updaterAci, R.drawable.ic_update_group_name_16)); + } + } else { + updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_name_has_changed_to_s, StringUtil.isolateBidi(groupDescriptionUpdate.newDescription)), R.drawable.ic_update_group_name_16)); + } + } + private void describeUnknownEditorNewDescription(@NonNull DecryptedGroupChange change, @NonNull List updates) { if (change.newDescription != null) { updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_description_has_changed), R.drawable.ic_update_group_name_16)); @@ -499,6 +1073,20 @@ final class GroupsV2UpdateMessageProducer { } } + private void describeAvatarChange(@NonNull GroupAvatarUpdate groupAvatarUpdate, @NonNull List updates) { + if (groupAvatarUpdate.updaterAci != null) { + boolean editorIsYou = selfIds.matches(groupAvatarUpdate.updaterAci); + + if (editorIsYou) { + updates.add(updateDescription(context.getString(R.string.MessageRecord_you_changed_the_group_avatar), R.drawable.ic_update_group_avatar_16)); + } else { + updates.add(updateDescription(R.string.MessageRecord_s_changed_the_group_avatar, groupAvatarUpdate.updaterAci, R.drawable.ic_update_group_avatar_16)); + } + } else { + updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_group_avatar_has_been_changed), R.drawable.ic_update_group_avatar_16)); + } + } + private void describeUnknownEditorNewAvatar(@NonNull DecryptedGroupChange change, @NonNull List updates) { if (change.newAvatar != null) { updates.add(updateDescription(context.getString(R.string.MessageRecord_the_group_group_avatar_has_been_changed), R.drawable.ic_update_group_avatar_16)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java index 2613298c2c..d7b988f8b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java @@ -56,7 +56,8 @@ public class InMemoryMessageRecord extends MessageRecord { false, -1, null, - 0); + 0, + null); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index ff5fbb97dd..95a1c6a0fa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList; import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context; import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails; +import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras; import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails; import org.thoughtcrime.securesms.database.model.databaseprotos.SessionSwitchoverEvent; import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent; @@ -104,6 +105,7 @@ public abstract class MessageRecord extends DisplayRecord { private final long receiptTimestamp; private final MessageId originalMessageId; private final int revisionNumber; + private final MessageExtras messageExtras; protected Boolean isJumboji = null; @@ -123,7 +125,8 @@ public abstract class MessageRecord extends DisplayRecord { boolean viewed, long receiptTimestamp, @Nullable MessageId originalMessageId, - int revisionNumber) + int revisionNumber, + @Nullable MessageExtras messageExtras) { super(body, fromRecipient, toRecipient, dateSent, dateReceived, threadId, deliveryStatus, hasDeliveryReceipt, type, @@ -143,6 +146,7 @@ public abstract class MessageRecord extends DisplayRecord { this.receiptTimestamp = receiptTimestamp; this.originalMessageId = originalMessageId; this.revisionNumber = revisionNumber; + this.messageExtras = messageExtras; } public abstract boolean isMms(); @@ -287,6 +291,10 @@ public abstract class MessageRecord extends DisplayRecord { return selfCreatedGroup(change); } + @Nullable public MessageExtras getMessageExtras() { + return messageExtras; + } + @VisibleForTesting @Nullable DecryptedGroupV2Context getDecryptedGroupV2Context() { if (!isGroupUpdate() || !isGroupV2()) { @@ -315,6 +323,30 @@ public abstract class MessageRecord extends DisplayRecord { try { byte[] decoded = Base64.decode(body); DecryptedGroupV2Context decryptedGroupV2Context = DecryptedGroupV2Context.ADAPTER.decode(decoded); + return getGv2ChangeDescription(context, decryptedGroupV2Context, recipientClickHandler); + } catch (IOException | IllegalArgumentException | IllegalStateException e) { + Log.w(TAG, "GV2 Message update detail could not be read", e); + return staticUpdateDescription(context.getString(R.string.MessageRecord_group_updated), R.drawable.ic_update_group_16); + } + } + + public static @NonNull UpdateDescription getGv2ChangeDescription(@NonNull Context context, @NonNull MessageExtras messageExtras, @Nullable Consumer recipientClickHandler) { + if (messageExtras.gv2UpdateDescription != null) { + if (messageExtras.gv2UpdateDescription.groupChangeUpdate != null) { + GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, SignalStore.account().getServiceIds(), recipientClickHandler); + + return UpdateDescription.concatWithNewLines(updateMessageProducer.describeChanges(messageExtras.gv2UpdateDescription.groupChangeUpdate.updates)); + } else if (messageExtras.gv2UpdateDescription.gv2ChangeDescription != null) { + return getGv2ChangeDescription(context, messageExtras.gv2UpdateDescription.gv2ChangeDescription, recipientClickHandler); + } else { + Log.w(TAG, "GV2 Update Description missing group change update!"); + } + } + return staticUpdateDescription(context.getString(R.string.MessageRecord_group_updated), R.drawable.ic_update_group_16); + } + + public static @NonNull UpdateDescription getGv2ChangeDescription(@NonNull Context context, @NonNull DecryptedGroupV2Context decryptedGroupV2Context, @Nullable Consumer recipientClickHandler) { + try { GroupsV2UpdateMessageProducer updateMessageProducer = new GroupsV2UpdateMessageProducer(context, SignalStore.account().getServiceIds(), recipientClickHandler); if (decryptedGroupV2Context.change != null && ((decryptedGroupV2Context.groupState != null && decryptedGroupV2Context.groupState.revision != 0) || decryptedGroupV2Context.previousGroupState != null)) { @@ -332,7 +364,7 @@ public abstract class MessageRecord extends DisplayRecord { } return UpdateDescription.concatWithNewLines(newGroupDescriptions); } - } catch (IOException | IllegalArgumentException | IllegalStateException e) { + } catch (IllegalArgumentException | IllegalStateException e) { Log.w(TAG, "GV2 Message update detail could not be read", e); return staticUpdateDescription(context.getString(R.string.MessageRecord_group_updated), R.drawable.ic_update_group_16); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java index 4d044a5da9..febd0af229 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java @@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList; import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge; +import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras; import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; @@ -111,12 +112,13 @@ public class MmsMessageRecord extends MessageRecord { @Nullable MessageId latestRevisionId, @Nullable MessageId originalMessageId, int revisionNumber, - boolean isRead) + boolean isRead, + @Nullable MessageExtras messageExtras) { super(id, body, fromRecipient, fromDeviceId, toRecipient, dateSent, dateReceived, dateServer, threadId, Status.STATUS_NONE, hasDeliveryReceipt, mailbox, mismatches, failures, subscriptionId, expiresIn, expireStarted, hasReadReceipt, - unidentified, reactions, remoteDelete, notifiedTimestamp, viewed, receiptTimestamp, originalMessageId, revisionNumber); + unidentified, reactions, remoteDelete, notifiedTimestamp, viewed, receiptTimestamp, originalMessageId, revisionNumber, messageExtras); this.slideDeck = slideDeck; this.quote = quote; @@ -299,7 +301,7 @@ public class MmsMessageRecord extends MessageRecord { getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(), hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, isRemoteDelete(), mentionsSelf, getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(), - getOriginalMessageId(), getRevisionNumber(), isRead()); + getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras()); } public @NonNull MmsMessageRecord withoutQuote() { @@ -307,7 +309,7 @@ public class MmsMessageRecord extends MessageRecord { getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(), hasReadReceipt(), null, getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf, getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(), - getOriginalMessageId(), getRevisionNumber(), isRead()); + getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras()); } public @NonNull MmsMessageRecord withAttachments(@NonNull List attachments) { @@ -329,7 +331,7 @@ public class MmsMessageRecord extends MessageRecord { getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(), hasReadReceipt(), quote, contacts, linkPreviews, isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf, getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(), - getOriginalMessageId(), getRevisionNumber(), isRead()); + getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras()); } public @NonNull MmsMessageRecord withPayment(@NonNull Payment payment) { @@ -337,7 +339,7 @@ public class MmsMessageRecord extends MessageRecord { getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(), hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf, getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), payment, getCall(), getScheduledDate(), getLatestRevisionId(), - getOriginalMessageId(), getRevisionNumber(), isRead()); + getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras()); } @@ -346,7 +348,7 @@ public class MmsMessageRecord extends MessageRecord { getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(), hasReadReceipt(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf, getNotifiedTimestamp(), isViewed(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), call, getScheduledDate(), getLatestRevisionId(), - getOriginalMessageId(), getRevisionNumber(), isRead()); + getOriginalMessageId(), getRevisionNumber(), isRead(), getMessageExtras()); } private static @NonNull List updateContacts(@NonNull List contacts, @NonNull Map attachmentIdMap) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java index abc1d4709d..fcbc5ba24e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ThreadRecord.java @@ -26,6 +26,7 @@ import org.thoughtcrime.securesms.database.MessageTypes; import org.thoughtcrime.securesms.database.ThreadTable; import org.thoughtcrime.securesms.database.ThreadTable.Extra; import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList; +import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExtras; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.whispersystems.signalservice.api.util.Preconditions; @@ -37,26 +38,27 @@ import java.util.Objects; */ public final class ThreadRecord { - private final long threadId; - private final String body; - private final Recipient recipient; - private final long type; - private final long date; - private final long deliveryStatus; - private final boolean hasDeliveryReceipt; - private final boolean hasReadReceipt; - private final Uri snippetUri; - private final String contentType; - private final Extra extra; - private final boolean meaningfulMessages; - private final int unreadCount; - private final boolean forcedUnread; - private final int distributionType; - private final boolean archived; - private final long expiresIn; - private final long lastSeen; - private final boolean isPinned; - private final int unreadSelfMentionsCount; + private final long threadId; + private final String body; + private final Recipient recipient; + private final long type; + private final long date; + private final long deliveryStatus; + private final boolean hasDeliveryReceipt; + private final boolean hasReadReceipt; + private final Uri snippetUri; + private final String contentType; + private final Extra extra; + private final boolean meaningfulMessages; + private final int unreadCount; + private final boolean forcedUnread; + private final int distributionType; + private final boolean archived; + private final long expiresIn; + private final long lastSeen; + private final boolean isPinned; + private final int unreadSelfMentionsCount; + private final MessageExtras messageExtras; private ThreadRecord(@NonNull Builder builder) { this.threadId = builder.threadId; @@ -79,6 +81,7 @@ public final class ThreadRecord { this.lastSeen = builder.lastSeen; this.isPinned = builder.isPinned; this.unreadSelfMentionsCount = builder.unreadSelfMentionsCount; + this.messageExtras = builder.messageExtras; } public long getThreadId() { @@ -189,6 +192,10 @@ public final class ThreadRecord { return extra != null && extra.isScheduled(); } + public @Nullable MessageExtras getMessageExtras() { + return messageExtras; + } + public @Nullable RecipientId getGroupAddedBy() { if (extra != null && extra.getGroupAddedBy() != null) return RecipientId.from(extra.getGroupAddedBy()); else return null; @@ -287,26 +294,27 @@ public final class ThreadRecord { } public static class Builder { - private long threadId; - private String body; - private Recipient recipient = Recipient.UNKNOWN; - private long type; - private long date; - private long deliveryStatus; - private boolean hasDeliveryReceipt; - private boolean hasReadReceipt; - private Uri snippetUri; - private String contentType; - private Extra extra; - private boolean meaningfulMessages; - private int unreadCount; - private boolean forcedUnread; - private int distributionType; - private boolean archived; - private long expiresIn; - private long lastSeen; - private boolean isPinned; - private int unreadSelfMentionsCount; + private long threadId; + private String body; + private Recipient recipient = Recipient.UNKNOWN; + private long type; + private long date; + private long deliveryStatus; + private boolean hasDeliveryReceipt; + private boolean hasReadReceipt; + private Uri snippetUri; + private String contentType; + private Extra extra; + private boolean meaningfulMessages; + private int unreadCount; + private boolean forcedUnread; + private int distributionType; + private boolean archived; + private long expiresIn; + private long lastSeen; + private boolean isPinned; + private int unreadSelfMentionsCount; + private MessageExtras messageExtras; public Builder(long threadId) { this.threadId = threadId; @@ -407,6 +415,11 @@ public final class ThreadRecord { return this; } + public Builder setSnippetMessageExtras(@Nullable MessageExtras messageExtras) { + this.messageExtras = messageExtras; + return this; + } + public Builder setUnreadSelfMentionsCount(int unreadSelfMentionsCount) { this.unreadSelfMentionsCount = unreadSelfMentionsCount; return this; diff --git a/app/src/main/protowire/Backup.proto b/app/src/main/protowire/Backup.proto index 6bcb277bbf..0299d77ffa 100644 --- a/app/src/main/protowire/Backup.proto +++ b/app/src/main/protowire/Backup.proto @@ -7,7 +7,6 @@ option java_package = "org.thoughtcrime.securesms.backup.v2.proto"; message BackupInfo { uint64 version = 1; uint64 backupTimeMs = 2; - bytes iv = 3; } message Frame { @@ -207,6 +206,9 @@ message ChatItem { repeated SendStatus sendStatus = 1; } + message DirectionlessMessageDetails { + } + uint64 chatId = 1; // conversation id uint64 authorId = 2; // recipient id uint64 dateSent = 3; @@ -217,8 +219,9 @@ message ChatItem { bool sms = 8; oneof directionalDetails { - IncomingMessageDetails incoming = 10; - OutgoingMessageDetails outgoing = 12; + IncomingMessageDetails incoming = 9; + OutgoingMessageDetails outgoing = 10; + DirectionlessMessageDetails directionless = 11; } oneof item { @@ -415,16 +418,17 @@ message FilePointer { optional bytes key = 5; optional string contentType = 6; + // Size of fullsize decrypted media blob in bytes. + // Can be ignored if unset/unavailable. optional uint32 size = 7; - optional bytes digest = 8; - optional bytes incrementalMac = 9; - optional bytes incrementalMacChunkSize = 10; - optional string fileName = 11; - optional uint32 flags = 12; - optional uint32 width = 13; - optional uint32 height = 14; - optional string caption = 15; - optional string blurHash = 16; + optional bytes incrementalMac = 8; + optional bytes incrementalMacChunkSize = 9; + optional string fileName = 10; + optional uint32 flags = 11; + optional uint32 width = 12; + optional uint32 height = 13; + optional string caption = 14; + optional string blurHash = 15; } message Quote { @@ -478,7 +482,7 @@ message Reaction { message ChatUpdateMessage { oneof update { SimpleChatUpdate simpleUpdate = 1; - GroupDescriptionChatUpdate groupDescription = 2; + GroupChangeChatUpdate groupChange = 2; ExpirationTimerChatUpdate expirationTimerChange = 3; ProfileChangeChatUpdate profileChange = 4; ThreadMergeChatUpdate threadMerge = 5; @@ -533,10 +537,6 @@ message SimpleChatUpdate { Type type = 1; } -message GroupDescriptionChatUpdate { - string newDescription = 1; -} - message ExpirationTimerChatUpdate { uint32 expiresInMs = 1; } @@ -554,6 +554,246 @@ message SessionSwitchoverChatUpdate { uint64 e164 = 1; } +message GroupChangeChatUpdate { + message Update { + // Note: group expiration timer changes are represented as ExpirationTimerChatUpdate. + oneof update { + GenericGroupUpdate genericGroupUpdate = 1; + GroupCreationUpdate groupCreationUpdate = 2; + GroupNameUpdate groupNameUpdate = 3; + GroupAvatarUpdate groupAvatarUpdate = 4; + GroupDescriptionUpdate groupDescriptionUpdate = 5; + GroupMembershipAccessLevelChangeUpdate groupMembershipAccessLevelChangeUpdate = 6; + GroupAttributesAccessLevelChangeUpdate groupAttributesAccessLevelChangeUpdate = 7; + GroupAnnouncementOnlyChangeUpdate groupAnnouncementOnlyChangeUpdate = 8; + GroupAdminStatusUpdate groupAdminStatusUpdate = 9; + GroupMemberLeftUpdate groupMemberLeftUpdate = 10; + GroupMemberRemovedUpdate groupMemberRemovedUpdate = 11; + SelfInvitedToGroupUpdate selfInvitedToGroupUpdate = 12; + SelfInvitedOtherUserToGroupUpdate selfInvitedOtherUserToGroupUpdate = 13; + GroupUnknownInviteeUpdate groupUnknownInviteeUpdate = 14; + GroupInvitationAcceptedUpdate groupInvitationAcceptedUpdate = 15; + GroupInvitationDeclinedUpdate groupInvitationDeclinedUpdate = 16; + GroupMemberJoinedUpdate groupMemberJoinedUpdate = 17; + GroupMemberAddedUpdate groupMemberAddedUpdate = 18; + GroupSelfInvitationRevokedUpdate groupSelfInvitationRevokedUpdate = 19; + GroupInvitationRevokedUpdate groupInvitationRevokedUpdate = 20; + GroupJoinRequestUpdate groupJoinRequestUpdate = 21; + GroupJoinRequestApprovalUpdate groupJoinRequestApprovalUpdate = 22; + GroupJoinRequestCanceledUpdate groupJoinRequestCanceledUpdate = 23; + GroupInviteLinkResetUpdate groupInviteLinkResetUpdate = 24; + GroupInviteLinkEnabledUpdate groupInviteLinkEnabledUpdate = 25; + GroupInviteLinkAdminApprovalUpdate groupInviteLinkAdminApprovalUpdate = 26; + GroupInviteLinkDisabledUpdate groupInviteLinkDisabledUpdate = 27; + GroupMemberJoinedByLinkUpdate groupMemberJoinedByLinkUpdate = 28; + GroupV2MigrationUpdate groupV2MigrationUpdate = 29; + GroupV2MigrationSelfInvitedUpdate groupV2MigrationSelfInvitedUpdate = 30; + GroupV2MigrationInvitedMembersUpdate groupV2MigrationInvitedMembersUpdate = 31; + GroupV2MigrationDroppedMembersUpdate groupV2MigrationDroppedMembersUpdate = 32; + GroupSequenceOfRequestsAndCancelsUpdate groupSequenceOfRequestsAndCancelsUpdate = 33; + } + } + + // Must be one or more; all updates batched together came from + // a single batched group state update. + repeated Update updates = 1; +} + +message GenericGroupUpdate { + optional bytes updaterAci = 1; +} + +message GroupCreationUpdate { + optional bytes updaterAci = 1; +} + +message GroupNameUpdate { + optional bytes updaterAci = 1; + // Null value means the group name was removed. + optional string newGroupName = 2; +} + +message GroupAvatarUpdate { + optional bytes updaterAci = 1; + bool wasRemoved = 2; +} + +message GroupDescriptionUpdate { + optional bytes updaterAci = 1; + // Null value means the group description was removed. + optional string newDescription = 2; +} + +enum GroupV2AccessLevel { + UNKNOWN = 0; + ANY = 1; + MEMBER = 2; + ADMINISTRATOR = 3; + UNSATISFIABLE = 4; +} + +message GroupMembershipAccessLevelChangeUpdate { + optional bytes updaterAci = 1; + GroupV2AccessLevel accessLevel = 2; +} + +message GroupAttributesAccessLevelChangeUpdate { + optional bytes updaterAci = 1; + GroupV2AccessLevel accessLevel = 2; +} + +message GroupAnnouncementOnlyChangeUpdate { + optional bytes updaterAci = 1; + bool isAnnouncementOnly = 2; +} + +message GroupAdminStatusUpdate { + optional bytes updaterAci = 1; + // The aci who had admin status granted or revoked. + bytes memberAci = 2; + bool wasAdminStatusGranted = 3; +} + +message GroupMemberLeftUpdate { + optional bytes aci = 1; +} + +message GroupMemberRemovedUpdate { + optional bytes removerAci = 1; + bytes removedAci = 2; +} + +message SelfInvitedToGroupUpdate { + optional bytes inviterAci = 1; +} + +message SelfInvitedOtherUserToGroupUpdate { + // If no invitee id available, use GroupUnknownInviteeUpdate + bytes inviteeServiceId = 1; +} + +message GroupUnknownInviteeUpdate { + // Can be the self user. + optional bytes inviterAci = 1; + uint32 inviteeCount = 2; +} + +message GroupInvitationAcceptedUpdate { + optional bytes inviterAci = 1; + bytes newMemberAci = 2; +} + +message GroupInvitationDeclinedUpdate { + optional bytes inviterAci = 1; + // Note: if invited by pni, just set inviteeAci to nil. + optional bytes inviteeAci = 2; +} + +message GroupMemberJoinedUpdate { + bytes newMemberAci = 1; +} + +message GroupMemberAddedUpdate { + optional bytes updaterAci = 1; + bytes newMemberAci = 2; + bool hadOpenInvitation = 3; + // If hadOpenInvitation is true, optionally include aci of the inviter. + optional bytes inviterAci = 4; +} + +// An invitation to self was revoked. +message GroupSelfInvitationRevokedUpdate { + optional bytes revokerAci = 1; +} + +// These invitees should never be the local user. +// Use GroupSelfInvitationRevokedUpdate in those cases. +// The inviter or updater can be the local user. +message GroupInvitationRevokedUpdate { + message Invitee { + optional bytes inviterAci = 1; + // Prefer to use aci over pni. No need to set + // pni if aci is set. Both can be missing. + optional bytes inviteeAci = 2; + optional bytes inviteePni = 3; + } + + // The member that revoked the invite(s), not the inviter! + // Assumed to be an admin (at the time, may no longer be an + // admin or even a member). + optional bytes updaterAci = 1; + repeated Invitee invitees = 2; +} + +message GroupJoinRequestUpdate { + bytes requestorAci = 1; +} + +message GroupJoinRequestApprovalUpdate { + bytes requestorAci = 1; + // The aci that approved or rejected the request. + optional bytes updaterAci = 2; + bool wasApproved = 3; +} + +message GroupJoinRequestCanceledUpdate { + bytes requestorAci = 1; +} + +// A single requestor has requested to join and cancelled +// their request repeatedly with no other updates in between. +// The last action encompassed by this update is always a +// cancellation; if there was another open request immediately +// after, it will be a separate GroupJoinRequestUpdate, either +// in the same frame or in a subsequent frame. +message GroupSequenceOfRequestsAndCancelsUpdate { + bytes requestorAci = 1; + uint32 count = 2; +} + +message GroupInviteLinkResetUpdate { + optional bytes updaterAci = 1; +} + +message GroupInviteLinkEnabledUpdate { + optional bytes updaterAci = 1; + bool linkRequiresAdminApproval = 2; +} + +message GroupInviteLinkAdminApprovalUpdate { + optional bytes updaterAci = 1; + bool linkRequiresAdminApproval = 2; +} + +message GroupInviteLinkDisabledUpdate { + optional bytes updaterAci = 1; +} + +message GroupMemberJoinedByLinkUpdate { + bytes newMemberAci = 1; +} + +// A gv1->gv2 migration occurred. +message GroupV2MigrationUpdate {} + +// Another user migrated gv1->gv2 but was unable to add +// the local user and invited them instead. +message GroupV2MigrationSelfInvitedUpdate {} + +// The local user migrated gv1->gv2 but was unable to +// add some members and invited them instead. +// (Happens if we don't have the invitee's profile key) +message GroupV2MigrationInvitedMembersUpdate { + int32 invitedMembersCount = 1; +} + +// The local user migrated gv1->gv2 but was unable to +// add or invite some members and dropped them instead. +// (Happens for e164 members where we don't have an aci). +message GroupV2MigrationDroppedMembersUpdate { + int32 droppedMembersCount = 1; +} + message StickerPack { bytes id = 1; bytes key = 2; diff --git a/app/src/main/protowire/Database.proto b/app/src/main/protowire/Database.proto index ced4593290..7cc4455cd0 100644 --- a/app/src/main/protowire/Database.proto +++ b/app/src/main/protowire/Database.proto @@ -11,6 +11,8 @@ package signal; option java_package = "org.thoughtcrime.securesms.database.model.databaseprotos"; option java_multiple_files = true; +import Backup.proto; + // DEPRECATED -- only here for database migrations message ReactionList { option deprecated = true; @@ -371,3 +373,12 @@ message ExternalLaunchTransactionState { GatewayRequest gatewayRequest = 2; string paymentSourceType = 3; } + +message MessageExtras { + GV2UpdateDescription gv2UpdateDescription = 1; +} + +message GV2UpdateDescription { + optional DecryptedGroupV2Context gv2ChangeDescription = 1; + backup.GroupChangeChatUpdate groupChangeUpdate = 2; +} \ No newline at end of file diff --git a/app/src/testShared/org/thoughtcrime/securesms/database/FakeMessageRecords.kt b/app/src/testShared/org/thoughtcrime/securesms/database/FakeMessageRecords.kt index a0fac5dcb3..fff425ea77 100644 --- a/app/src/testShared/org/thoughtcrime/securesms/database/FakeMessageRecords.kt +++ b/app/src/testShared/org/thoughtcrime/securesms/database/FakeMessageRecords.kt @@ -183,7 +183,8 @@ object FakeMessageRecords { null, null, 0, - false + false, + null ) } }