Sync release note channel settings with storage service.

This commit is contained in:
Cody Henthorne
2026-05-14 16:24:28 -04:00
committed by jeffrey-signal
parent 9f608337f1
commit 6eea4ba937
8 changed files with 108 additions and 8 deletions
@@ -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()))
}
@@ -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)
@@ -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;
}
@@ -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)
@@ -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")
@@ -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<RecipientRecord>): List<AccountRecord.PinnedConversation> {
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(
@@ -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()
}
@@ -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 {