Add support for avatar colors in storage service.

This commit is contained in:
Greyson Parrelli
2025-03-05 15:41:35 -05:00
committed by Michelle Tang
parent 93d18c1763
commit 83611414cc
9 changed files with 135 additions and 5 deletions

View File

@@ -45,6 +45,7 @@ import org.thoughtcrime.securesms.migrations.ApplyUnknownFieldsToSelfMigrationJo
import org.thoughtcrime.securesms.migrations.AttachmentCleanupMigrationJob;
import org.thoughtcrime.securesms.migrations.AttachmentHashBackfillMigrationJob;
import org.thoughtcrime.securesms.migrations.AttributesMigrationJob;
import org.thoughtcrime.securesms.migrations.AvatarColorStorageServiceMigrationJob;
import org.thoughtcrime.securesms.migrations.AvatarIdRemovalMigrationJob;
import org.thoughtcrime.securesms.migrations.AvatarMigrationJob;
import org.thoughtcrime.securesms.migrations.BackfillDigestsForDuplicatesMigrationJob;
@@ -271,6 +272,7 @@ public final class JobManagerFactories {
put(AttachmentCleanupMigrationJob.KEY, new AttachmentCleanupMigrationJob.Factory());
put(AttachmentHashBackfillMigrationJob.KEY, new AttachmentHashBackfillMigrationJob.Factory());
put(AttributesMigrationJob.KEY, new AttributesMigrationJob.Factory());
put(AvatarColorStorageServiceMigrationJob.KEY, new AvatarColorStorageServiceMigrationJob.Factory());
put(AvatarIdRemovalMigrationJob.KEY, new AvatarIdRemovalMigrationJob.Factory());
put(AvatarMigrationJob.KEY, new AvatarMigrationJob.Factory());
put(BackfillDigestsMigrationJob.KEY, new BackfillDigestsMigrationJob.Factory());

View File

@@ -107,9 +107,7 @@ import java.util.stream.Collectors
* == Syncing a new field on an existing record ==
*
* - Add the field the the respective proto
* - Update the respective model (i.e. [SignalContactRecord])
* - Add getters
* - Update the builder
* - Update [StorageSyncModels]
* - Update the respective record processor (i.e [ContactRecordProcessor]). You need to make sure that you're:
* - Merging the attributes, likely preferring remote
* - Adding to doParamsMatch()

View File

@@ -173,9 +173,10 @@ public class ApplicationMigrations {
static final int FTS_TRIGGER_FIX = 129;
static final int THREAD_TABLE_PINNED_MIGRATION = 130;
static final int GROUP_DECLINE_INVITE_FIX = 131;
static final int AVATAR_COLOR_MIGRATION_JOB = 132;
}
public static final int CURRENT_VERSION = 131;
public static final int CURRENT_VERSION = 132;
/**
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
@@ -798,6 +799,10 @@ public class ApplicationMigrations {
jobs.put(Version.GROUP_DECLINE_INVITE_FIX, new DatabaseMigrationJob());
}
if (lastSeenVersion < Version.AVATAR_COLOR_MIGRATION_JOB) {
jobs.put(Version.AVATAR_COLOR_MIGRATION_JOB, new AvatarColorStorageServiceMigrationJob());
}
return jobs;
}

View File

@@ -0,0 +1,71 @@
package org.thoughtcrime.securesms.migrations
import org.signal.core.util.logging.Log
import org.signal.core.util.requireLong
import org.signal.core.util.select
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.RecipientTable.Companion.ID
import org.thoughtcrime.securesms.database.RecipientTable.Companion.STORAGE_SERVICE_ID
import org.thoughtcrime.securesms.database.RecipientTable.Companion.TABLE_NAME
import org.thoughtcrime.securesms.database.RecipientTable.Companion.TYPE
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.storage.StorageSyncHelper
/**
* A job that marks all contacts and groups as needing to be synced, so that we'll update the
* storage records with the new avatar color field.
*/
internal class AvatarColorStorageServiceMigrationJob(
parameters: Parameters = Parameters.Builder().build()
) : MigrationJob(parameters) {
companion object {
val TAG = Log.tag(AvatarColorStorageServiceMigrationJob::class.java)
const val KEY = "AvatarColorStorageServiceMigrationJob"
}
override fun getFactoryKey(): String = KEY
override fun isUiBlocking(): Boolean = false
override fun performMigration() {
if (!Recipient.isSelfSet) {
return
}
if (!SignalStore.account.isRegistered) {
return
}
SignalDatabase.recipients.markNeedsSync(Recipient.self().id)
SignalDatabase.recipients.markAllContactsAndGroupsAsNeedsSync()
StorageSyncHelper.scheduleSyncForDataChange()
}
override fun shouldRetry(e: Exception): Boolean = false
private fun RecipientTable.markAllContactsAndGroupsAsNeedsSync() {
writableDatabase.withinTransaction { db ->
db.select(ID)
.from(TABLE_NAME)
.where("$STORAGE_SERVICE_ID NOT NULL AND $TYPE IN (${RecipientTable.RecipientType.INDIVIDUAL.id}, ${RecipientTable.RecipientType.GV2.id})")
.run()
.use { cursor ->
while (cursor.moveToNext()) {
rotateStorageId(RecipientId.from(cursor.requireLong(ID)))
}
}
}
}
class Factory : Job.Factory<AvatarColorStorageServiceMigrationJob> {
override fun create(parameters: Parameters, serializedData: ByteArray?): AvatarColorStorageServiceMigrationJob {
return AvatarColorStorageServiceMigrationJob(parameters)
}
}
}

View File

@@ -231,6 +231,7 @@ class ContactRecordProcessor(
nickname = remote.proto.nickname
pniSignatureVerified = remote.proto.pniSignatureVerified || local.proto.pniSignatureVerified
note = remote.proto.note.nullIfBlank() ?: ""
avatarColor = local.proto.avatarColor
}.build().toSignalContactRecord(StorageId.forContact(keyGenerator.generate()))
val matchesRemote = doParamsMatch(remote, merged)

View File

@@ -58,6 +58,7 @@ class GroupV2RecordProcessor(private val recipientTable: RecipientTable, private
dontNotifyForMentionsIfMuted = remote.proto.dontNotifyForMentionsIfMuted
hideStory = remote.proto.hideStory
storySendMode = remote.proto.storySendMode
avatarColor = local.proto.avatarColor
}.build().toSignalGroupV2Record(StorageId.forGroupV2(keyGenerator.generate()))
val matchesRemote = doParamsMatch(remote, merged)

View File

@@ -162,6 +162,7 @@ object StorageSyncHelper {
storyViewReceiptsEnabled = storyViewReceiptsState
hasSeenGroupStoryEducationSheet = SignalStore.story.userHasSeenGroupStoryEducationSheet
hasCompletedUsernameOnboarding = SignalStore.uiHints.hasCompletedUsernameOnboarding()
avatarColor = StorageSyncModels.localToRemoteAvatarColor(self.avatarColor)
username = SignalStore.account.username ?: ""
usernameLink = SignalStore.account.usernameLink?.let { linkComponents ->
AccountRecord.UsernameLink(

View File

@@ -6,6 +6,7 @@ import org.signal.core.util.isNotEmpty
import org.signal.core.util.isNullOrEmpty
import org.signal.libsignal.zkgroup.groups.GroupMasterKey
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.GroupTable.ShowAsStoryState
import org.thoughtcrime.securesms.database.IdentityTable.VerifiedStatus
import org.thoughtcrime.securesms.database.RecipientTable
@@ -41,6 +42,7 @@ import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.Id
import org.whispersystems.signalservice.internal.storage.protos.GroupV2Record
import java.util.Currency
import kotlin.math.max
import org.whispersystems.signalservice.internal.storage.protos.AvatarColor as RemoteAvatarColor
object StorageSyncModels {
@@ -182,6 +184,7 @@ object StorageSyncModels {
pniSignatureVerified = recipient.syncExtras.pniSignatureVerified
nickname = recipient.nickname.takeUnless { it.isEmpty }?.let { ContactRecord.Name(given = it.givenName, family = it.familyName) }
note = recipient.note ?: ""
avatarColor = localToRemoteAvatarColor(recipient.avatarColor)
}.build().toSignalContactRecord(StorageId.forContact(rawStorageId))
}
@@ -218,6 +221,7 @@ object StorageSyncModels {
mutedUntilTimestamp = recipient.muteUntil
dontNotifyForMentionsIfMuted = recipient.mentionSetting == RecipientTable.MentionSetting.ALWAYS_NOTIFY
hideStory = recipient.extras != null && recipient.extras.hideStory()
avatarColor = localToRemoteAvatarColor(recipient.avatarColor)
storySendMode = when (groups.getShowAsStoryState(groupId)) {
ShowAsStoryState.ALWAYS -> GroupV2Record.StorySendMode.ENABLED
ShowAsStoryState.NEVER -> GroupV2Record.StorySendMode.DISABLED
@@ -341,4 +345,23 @@ object StorageSyncModels {
return null
}
}
fun localToRemoteAvatarColor(avatarColor: AvatarColor): RemoteAvatarColor {
return when (avatarColor) {
AvatarColor.A100 -> RemoteAvatarColor.A100
AvatarColor.A110 -> RemoteAvatarColor.A110
AvatarColor.A120 -> RemoteAvatarColor.A120
AvatarColor.A130 -> RemoteAvatarColor.A130
AvatarColor.A140 -> RemoteAvatarColor.A140
AvatarColor.A150 -> RemoteAvatarColor.A150
AvatarColor.A160 -> RemoteAvatarColor.A160
AvatarColor.A170 -> RemoteAvatarColor.A170
AvatarColor.A180 -> RemoteAvatarColor.A180
AvatarColor.A190 -> RemoteAvatarColor.A190
AvatarColor.A200 -> RemoteAvatarColor.A200
AvatarColor.A210 -> RemoteAvatarColor.A210
AvatarColor.UNKNOWN -> RemoteAvatarColor.A100
AvatarColor.ON_SURFACE_VARIANT -> RemoteAvatarColor.A100
}
}
}

View File

@@ -75,6 +75,31 @@ message StorageRecord {
}
}
// If unset - computed as the value of the first byte of SHA-256(msg=CONTACT_ID)
// modulo the count of colors. Once set the avatar color for a recipient is
// never recomputed or changed.
//
// `CONTACT_ID` is the first available identifier from the list:
// - ServiceIdToBinary(ACI)
// - E164
// - ServiceIdToBinary(PNI)
// - Group Id
enum AvatarColor {
A100 = 0;
A110 = 1;
A120 = 2;
A130 = 3;
A140 = 4;
A150 = 5;
A160 = 6;
A170 = 7;
A180 = 8;
A190 = 9;
A200 = 10;
A210 = 11;
}
message ContactRecord {
enum IdentityState {
DEFAULT = 0;
@@ -110,7 +135,8 @@ message ContactRecord {
bool pniSignatureVerified = 21;
Name nickname = 22;
string note = 23;
// NEXT ID: 24
optional AvatarColor avatarColor = 24;
// Next ID: 25
}
message GroupV1Record {
@@ -139,6 +165,7 @@ message GroupV2Record {
bool hideStory = 8;
reserved /* storySendEnabled */ 9;
StorySendMode storySendMode = 10;
optional AvatarColor avatarColor = 11;
}
message Payments {
@@ -237,6 +264,7 @@ message AccountRecord {
optional bool hasBackup = 39; // Set to true after backups are enabled and one is uploaded.
optional uint64 backupTier = 40; // See zkgroup for integer particular values
IAPSubscriberData backupSubscriberData = 41;
optional AvatarColor avatarColor = 42;
}
message StoryDistributionListRecord {