diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index d3e72b4e24..aa9bb08078 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -4028,6 +4028,11 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da fun rotateStorageId(recipientId: RecipientId, logFailure: Boolean = false) { val selfId = Recipient.self().id + if (recipientId != selfId && recipientId == SignalStore.releaseChannel.releaseChannelRecipientId) { + // Release channel info is stored on the account record (self) + rotateStorageId(selfId) + } + val values = ContentValues(1).apply { put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(StorageSyncHelper.generateKey())) } 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 c1aa038865..78fbaeda97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt @@ -1622,6 +1622,8 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa Log.w(TAG, "Failed to parse serviceId!") null } + } else if (pinned.releaseNotes != null) { + SignalStore.releaseChannel.releaseChannelRecipientId?.let { Recipient.resolved(it) } } else if (pinned.legacyGroupId != null) { try { Recipient.externalGroupExact(GroupId.v1(pinned.legacyGroupId!!.toByteArray())) @@ -1657,6 +1659,10 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa notifyConversationListListeners() } + fun applyStorageSyncReleaseChannelUpdate(recipientId: RecipientId, archived: Boolean, forcedUnread: Boolean) { + applyStorageSyncUpdate(recipientId, archived, forcedUnread, isGroup = false) + } + private fun applyStorageSyncUpdate(recipientId: RecipientId, archived: Boolean, forcedUnread: Boolean, isGroup: Boolean) { val values = ContentValues() values.put(ARCHIVED, if (archived) 1 else 0) diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 335554e9e7..e443946c31 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -199,9 +199,10 @@ public class ApplicationMigrations { static final int COLLAPSED_EVENTS = 155; static final int COLLAPSED_EVENTS_2 = 156; static final int KEY_TRANSPARENCY = 157; + static final int RELEASE_NOTES_CHAT_SYNC = 158; } - public static final int CURRENT_VERSION = 157; + public static final int CURRENT_VERSION = 158; /** * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call @@ -924,6 +925,10 @@ public class ApplicationMigrations { jobs.put(Version.KEY_TRANSPARENCY, new ResetKeyTransparencyMigrationJob()); } + if (lastSeenVersion < Version.RELEASE_NOTES_CHAT_SYNC) { + jobs.put(Version.RELEASE_NOTES_CHAT_SYNC, new AccountRecordMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.kt index e3a00b8b9b..817aa9e6dc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.kt @@ -140,6 +140,10 @@ class AccountRecordProcessor( backupTier = local.proto.backupTier ?: remote.proto.backupTier automaticKeyVerificationDisabled = remote.proto.automaticKeyVerificationDisabled hasSeenAdminDeleteEducationDialog = remote.proto.hasSeenAdminDeleteEducationDialog + releaseNotesChatArchived = remote.proto.releaseNotesChatArchived + releaseNotesChatMutedUntilTimestamp = remote.proto.releaseNotesChatMutedUntilTimestamp + releaseNotesChatBlocked = remote.proto.releaseNotesChatBlocked + releaseNotesChatMarkedUnread = remote.proto.releaseNotesChatMarkedUnread safeSetPayments(payments?.enabled == true, payments?.entropy?.toByteArray()) safeSetSubscriber(donationSubscriberId, donationSubscriberCurrencyCode) diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.kt index 6cbbd8f421..725e58b2fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncHelper.kt @@ -139,6 +139,8 @@ object StorageSyncHelper { val storageId = selfRecord?.storageId ?: self.storageId + val releaseChannelRecord: RecipientRecord? = SignalStore.releaseChannel.releaseChannelRecipientId?.let { SignalDatabase.recipients.getRecordForSync(it) } + val accountRecord = SignalAccountRecord.newBuilder(selfRecord?.syncExtras?.storageProto).apply { profileKey = self.profileKey?.toByteString() ?: ByteString.EMPTY givenName = self.profileName.givenName @@ -197,6 +199,13 @@ object StorageSyncHelper { safeSetPayments(SignalStore.payments.mobileCoinPaymentsEnabled(), Optional.ofNullable(SignalStore.payments.paymentsEntropy).map { obj: Entropy -> obj.bytes }.orElse(null)) automaticKeyVerificationDisabled = !SignalStore.settings.automaticVerificationEnabled hasSeenAdminDeleteEducationDialog = SignalStore.uiHints.hasSeenAdminDeleteEducationDialog() + + if (releaseChannelRecord != null) { + releaseNotesChatArchived = releaseChannelRecord.syncExtras.isArchived == true + releaseNotesChatMutedUntilTimestamp = releaseChannelRecord.muteUntil + releaseNotesChatBlocked = releaseChannelRecord.isBlocked == true + releaseNotesChatMarkedUnread = releaseChannelRecord.syncExtras.isForcedUnread == true + } } return accountRecord.toSignalAccountRecord(StorageId.forAccount(storageId)).toSignalStorageRecord() @@ -308,6 +317,13 @@ object StorageSyncHelper { SignalStore.misc.usernameQrCodeColorScheme = StorageSyncModels.remoteToLocalUsernameColor(update.new.proto.usernameLink!!.color) } + SignalStore.releaseChannel.releaseChannelRecipientId?.let { releaseChannelId -> + SignalDatabase.recipients.setBlocked(releaseChannelId, update.new.proto.releaseNotesChatBlocked) + SignalDatabase.recipients.setMuted(releaseChannelId, update.new.proto.releaseNotesChatMutedUntilTimestamp) + SignalDatabase.threads.applyStorageSyncReleaseChannelUpdate(releaseChannelId, update.new.proto.releaseNotesChatArchived, update.new.proto.releaseNotesChatMarkedUnread) + Recipient.live(releaseChannelId).refresh() + } + if (update.new.proto.notificationProfileManualOverride != null) { if (update.new.proto.notificationProfileManualOverride!!.enabled != null) { Log.i(TAG, "Found a remote enabled notification override") diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.kt b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.kt index 1684d7839a..ee360ee9d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.kt @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.InAppPaymentData import org.thoughtcrime.securesms.groups.BadGroupIdException import org.thoughtcrime.securesms.groups.GroupId import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId @@ -123,13 +124,17 @@ object StorageSyncModels { @JvmStatic fun localToRemotePinnedConversations(records: List): List { + val releaseChannelId = SignalStore.releaseChannel.releaseChannelRecipientId return records - .filter { it.recipientType == RecipientType.GV1 || it.recipientType == RecipientType.GV2 || it.registered == RecipientTable.RegisteredState.REGISTERED } - .map { localToRemotePinnedConversation(it) } + .filter { it.recipientType == RecipientType.GV1 || it.recipientType == RecipientType.GV2 || it.registered == RecipientTable.RegisteredState.REGISTERED || it.id == releaseChannelId } + .map { localToRemotePinnedConversation(it, releaseChannelId) } } @JvmStatic - private fun localToRemotePinnedConversation(settings: RecipientRecord): AccountRecord.PinnedConversation { + private fun localToRemotePinnedConversation(settings: RecipientRecord, releaseChannelId: RecipientId?): AccountRecord.PinnedConversation { + if (settings.id == releaseChannelId) { + return AccountRecord.PinnedConversation(releaseNotes = AccountRecord.PinnedConversation.ReleaseNotes()) + } return when (settings.recipientType) { RecipientType.INDIVIDUAL -> { AccountRecord.PinnedConversation( diff --git a/app/src/spinner/java/org/thoughtcrime/securesms/StorageServicePlugin.kt b/app/src/spinner/java/org/thoughtcrime/securesms/StorageServicePlugin.kt index 3749de5237..b967bbf0d5 100644 --- a/app/src/spinner/java/org/thoughtcrime/securesms/StorageServicePlugin.kt +++ b/app/src/spinner/java/org/thoughtcrime/securesms/StorageServicePlugin.kt @@ -33,7 +33,7 @@ class StorageServicePlugin : Plugin { if (record.proto.account != null) { row += "Account" - row += record.proto.account.toString() + row += record.proto.account.toString().prettyPrintProto() } else if (record.proto.contact != null) { row += "Contact" row += record.proto.toString() @@ -77,3 +77,55 @@ class StorageServicePlugin : Plugin { const val PATH = "/storage" } } + +private fun String.prettyPrintProto(): String { + val out = StringBuilder(length + length / 4) + var indent = 0 + var compactDepth = 0 + fun newline() { + out.append('\n').append(" ".repeat(indent)) + } + var i = 0 + while (i < length) { + val c = this[i] + when (c) { + '{', '[' -> { + val compact = c == '[' && this.regionMatches(i + 1, "hex", 0, 3, ignoreCase = true) + if (compact) { + compactDepth++ + out.append(c) + } else { + indent++ + out.append(c) + newline() + } + } + '}', ']' -> { + if (compactDepth > 0 && c == ']') { + compactDepth-- + out.append(c) + } else { + indent = (indent - 1).coerceAtLeast(0) + val opener = if (c == '}') '{' else '[' + while (out.isNotEmpty() && (out.last() == ' ' || out.last() == '\n')) { + out.deleteCharAt(out.length - 1) + } + if (out.isNotEmpty() && out.last() == opener) { + out.append(c) + } else { + newline() + out.append(c) + } + } + } + ',' -> { + out.append(c) + if (compactDepth == 0) newline() + } + ' ' -> if (out.isNotEmpty() && out.last() != '\n' && out.last() != ' ') out.append(c) + else -> out.append(c) + } + i++ + } + return out.toString() +} diff --git a/lib/libsignal-service/src/main/protowire/StorageService.proto b/lib/libsignal-service/src/main/protowire/StorageService.proto index dd232caca2..4bc239f578 100644 --- a/lib/libsignal-service/src/main/protowire/StorageService.proto +++ b/lib/libsignal-service/src/main/protowire/StorageService.proto @@ -195,10 +195,13 @@ message AccountRecord { bytes serviceIdBinary = 3; // service ID binary (i.e. 16 byte UUID for ACI, 1 byte prefix + 16 byte UUID for PNI) } + message ReleaseNotes {} + oneof identifier { - Contact contact = 1; - bytes legacyGroupId = 3; - bytes groupMasterKey = 4; + Contact contact = 1; + bytes legacyGroupId = 3; + bytes groupMasterKey = 4; + ReleaseNotes releaseNotes = 5; } } @@ -298,6 +301,10 @@ message AccountRecord { bool notificationProfileSyncDisabled = 45; bool automaticKeyVerificationDisabled = 46; bool hasSeenAdminDeleteEducationDialog = 47; + bool releaseNotesChatArchived = 48; + uint64 releaseNotesChatMutedUntilTimestamp = 49; + bool releaseNotesChatBlocked = 50; + bool releaseNotesChatMarkedUnread = 51; } message StoryDistributionListRecord {