From 5e03e31ffd02a01486508a219846d1873de0e950 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Tue, 30 Jul 2024 17:02:57 -0400 Subject: [PATCH] Fix various AccountData backupV2 import/export bugs. I'm not including the auto-generated test files yet because I'm still making tweaks, but these are all valid fixes that got the current (uncommitted) batch of test files passing. --- .../RecipientTableBackupExtensions.kt | 31 +++-- .../database/ThreadTableBackupExtensions.kt | 97 +-------------- .../v2/processor/AccountDataProcessor.kt | 22 +++- .../backup/v2/util/ModelConverters.kt | 112 ++++++++++++++++++ 4 files changed, 161 insertions(+), 101 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ModelConverters.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt index 9ab7375132..2c30843550 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/RecipientTableBackupExtensions.kt @@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.groups.v2.processing.GroupsV2StateProcessor import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.profiles.ProfileName +import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.storage.StorageSyncHelper import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations @@ -180,11 +181,11 @@ fun RecipientTable.restoreContactFromBackup(contact: Contact): RecipientId { val profileKey = contact.profileKey?.toByteArray() val values = contentValuesOf( RecipientTable.BLOCKED to contact.blocked, - RecipientTable.HIDDEN to (contact.visibility == Contact.Visibility.HIDDEN), + RecipientTable.HIDDEN to contact.visibility.toLocal().serialize(), RecipientTable.TYPE to RecipientTable.RecipientType.INDIVIDUAL.id, - RecipientTable.PROFILE_FAMILY_NAME to contact.profileFamilyName.nullIfBlank(), - RecipientTable.PROFILE_GIVEN_NAME to contact.profileGivenName.nullIfBlank(), - RecipientTable.PROFILE_JOINED_NAME to ProfileName.fromParts(contact.profileGivenName.nullIfBlank(), contact.profileFamilyName.nullIfBlank()).toString().nullIfBlank(), + RecipientTable.PROFILE_FAMILY_NAME to contact.profileFamilyName, + RecipientTable.PROFILE_GIVEN_NAME to contact.profileGivenName, + RecipientTable.PROFILE_JOINED_NAME to ProfileName.fromParts(contact.profileGivenName, contact.profileFamilyName).toString(), RecipientTable.PROFILE_KEY to if (profileKey == null) null else Base64.encodeWithPadding(profileKey), RecipientTable.PROFILE_SHARING to contact.profileSharing.toInt(), RecipientTable.USERNAME to contact.username, @@ -249,6 +250,14 @@ fun RecipientTable.restoreGroupFromBackup(group: Group): RecipientId { return RecipientId.from(recipientId) } +private fun Contact.Visibility.toLocal(): Recipient.HiddenState { + return when (this) { + Contact.Visibility.VISIBLE -> Recipient.HiddenState.NOT_HIDDEN + Contact.Visibility.HIDDEN -> Recipient.HiddenState.HIDDEN + Contact.Visibility.HIDDEN_MESSAGE_REQUEST -> Recipient.HiddenState.HIDDEN_MESSAGE_REQUEST + } +} + private fun Group.AccessControl.AccessRequired.toLocal(): AccessControl.AccessRequired { return when (this) { Group.AccessControl.AccessRequired.UNKNOWN -> AccessControl.AccessRequired.UNKNOWN @@ -434,11 +443,11 @@ class BackupContactIterator(private val cursor: Cursor, private val selfId: Long .username(cursor.requireString(RecipientTable.USERNAME)) .e164(cursor.requireString(RecipientTable.E164)?.e164ToLong()) .blocked(cursor.requireBoolean(RecipientTable.BLOCKED)) - .visibility(if (cursor.requireBoolean(RecipientTable.HIDDEN)) Contact.Visibility.HIDDEN else Contact.Visibility.VISIBLE) + .visibility(Recipient.HiddenState.deserialize(cursor.requireInt(RecipientTable.HIDDEN)).toRemote()) .profileKey(if (profileKey != null) Base64.decode(profileKey).toByteString() else null) .profileSharing(cursor.requireBoolean(RecipientTable.PROFILE_SHARING)) - .profileGivenName(cursor.requireString(RecipientTable.PROFILE_GIVEN_NAME).nullIfBlank()) - .profileFamilyName(cursor.requireString(RecipientTable.PROFILE_FAMILY_NAME).nullIfBlank()) + .profileGivenName(cursor.requireString(RecipientTable.PROFILE_GIVEN_NAME)) + .profileFamilyName(cursor.requireString(RecipientTable.PROFILE_FAMILY_NAME)) .hideStory(extras?.hideStory() ?: false) if (registeredState == RecipientTable.RegisteredState.REGISTERED) { @@ -458,6 +467,14 @@ class BackupContactIterator(private val cursor: Cursor, private val selfId: Long } } +private fun Recipient.HiddenState.toRemote(): Contact.Visibility { + return when (this) { + Recipient.HiddenState.NOT_HIDDEN -> return Contact.Visibility.VISIBLE + Recipient.HiddenState.HIDDEN -> return Contact.Visibility.HIDDEN + Recipient.HiddenState.HIDDEN_MESSAGE_REQUEST -> return Contact.Visibility.HIDDEN_MESSAGE_REQUEST + } +} + /** * Provides a nice iterable interface over a [RecipientTable] cursor, converting rows to [BackupRecipient]s. * Important: Because this is backed by a cursor, you must close it. It's recommended to use `.use()` or try-with-resources. diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ThreadTableBackupExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ThreadTableBackupExtensions.kt index 36db382bcc..792c8127a1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ThreadTableBackupExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/database/ThreadTableBackupExtensions.kt @@ -18,9 +18,9 @@ import org.signal.core.util.requireLong import org.signal.core.util.toInt import org.thoughtcrime.securesms.backup.v2.ImportState import org.thoughtcrime.securesms.backup.v2.proto.Chat -import org.thoughtcrime.securesms.backup.v2.proto.ChatStyle +import org.thoughtcrime.securesms.backup.v2.util.BackupConverters +import org.thoughtcrime.securesms.backup.v2.util.toLocal import org.thoughtcrime.securesms.conversation.colors.ChatColors -import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette import org.thoughtcrime.securesms.database.RecipientTable import org.thoughtcrime.securesms.database.ThreadTable import org.thoughtcrime.securesms.database.model.databaseprotos.ChatColor @@ -59,7 +59,7 @@ fun ThreadTable.clearAllDataForBackupRestore() { } fun ThreadTable.restoreFromBackup(chat: Chat, recipientId: RecipientId, importState: ImportState): Long? { - val chatColor = chat.style?.remoteToLocalChatColors(importState) + val chatColor = chat.style?.toLocal(importState) // TODO [backup] Wallpaper @@ -110,24 +110,6 @@ class ChatExportIterator(private val cursor: Cursor) : Iterator, Closeable } } - var chatStyleBuilder: ChatStyle.Builder? = null - if (chatColors != null) { - chatStyleBuilder = ChatStyle.Builder() - when (chatColorId) { - ChatColors.Id.NotSet -> {} - ChatColors.Id.Auto -> { - chatStyleBuilder.autoBubbleColor = ChatStyle.AutomaticBubbleColor() - } - ChatColors.Id.BuiltIn -> { - chatStyleBuilder.bubbleColorPreset = chatColors.localToRemoteChatColors() - } - is ChatColors.Id.Custom -> { - chatStyleBuilder.customColorId = chatColorId.longValue - } - } - } - // TODO [backup] wallpaper - return Chat( id = cursor.requireLong(ThreadTable.ID), recipientId = cursor.requireLong(ThreadTable.RECIPIENT_ID), @@ -137,7 +119,7 @@ class ChatExportIterator(private val cursor: Cursor) : Iterator, Closeable muteUntilMs = cursor.requireLong(RecipientTable.MUTE_UNTIL), markedUnread = ThreadTable.ReadStatus.deserialize(cursor.requireInt(ThreadTable.READ)) == ThreadTable.ReadStatus.FORCED_UNREAD, dontNotifyForMentionsIfMuted = RecipientTable.MentionSetting.DO_NOT_NOTIFY.id == cursor.requireInt(RecipientTable.MENTION_SETTING), - style = chatStyleBuilder?.build() + style = BackupConverters.constructRemoteChatStyle(chatColors, chatColorId) ) } @@ -145,74 +127,3 @@ class ChatExportIterator(private val cursor: Cursor) : Iterator, Closeable cursor.close() } } - -private fun ChatStyle.remoteToLocalChatColors(importState: ImportState): ChatColors? { - if (this.bubbleColorPreset != null) { - return when (this.bubbleColorPreset) { - ChatStyle.BubbleColorPreset.SOLID_CRIMSON -> ChatColorsPalette.Bubbles.CRIMSON - ChatStyle.BubbleColorPreset.SOLID_VERMILION -> ChatColorsPalette.Bubbles.VERMILION - ChatStyle.BubbleColorPreset.SOLID_BURLAP -> ChatColorsPalette.Bubbles.BURLAP - ChatStyle.BubbleColorPreset.SOLID_FOREST -> ChatColorsPalette.Bubbles.FOREST - ChatStyle.BubbleColorPreset.SOLID_WINTERGREEN -> ChatColorsPalette.Bubbles.WINTERGREEN - ChatStyle.BubbleColorPreset.SOLID_TEAL -> ChatColorsPalette.Bubbles.TEAL - ChatStyle.BubbleColorPreset.SOLID_BLUE -> ChatColorsPalette.Bubbles.BLUE - ChatStyle.BubbleColorPreset.SOLID_INDIGO -> ChatColorsPalette.Bubbles.INDIGO - ChatStyle.BubbleColorPreset.SOLID_VIOLET -> ChatColorsPalette.Bubbles.VIOLET - ChatStyle.BubbleColorPreset.SOLID_PLUM -> ChatColorsPalette.Bubbles.PLUM - ChatStyle.BubbleColorPreset.SOLID_TAUPE -> ChatColorsPalette.Bubbles.TAUPE - ChatStyle.BubbleColorPreset.SOLID_STEEL -> ChatColorsPalette.Bubbles.STEEL - ChatStyle.BubbleColorPreset.GRADIENT_EMBER -> ChatColorsPalette.Bubbles.EMBER - ChatStyle.BubbleColorPreset.GRADIENT_MIDNIGHT -> ChatColorsPalette.Bubbles.MIDNIGHT - ChatStyle.BubbleColorPreset.GRADIENT_INFRARED -> ChatColorsPalette.Bubbles.INFRARED - ChatStyle.BubbleColorPreset.GRADIENT_LAGOON -> ChatColorsPalette.Bubbles.LAGOON - ChatStyle.BubbleColorPreset.GRADIENT_FLUORESCENT -> ChatColorsPalette.Bubbles.FLUORESCENT - ChatStyle.BubbleColorPreset.GRADIENT_BASIL -> ChatColorsPalette.Bubbles.BASIL - ChatStyle.BubbleColorPreset.GRADIENT_SUBLIME -> ChatColorsPalette.Bubbles.SUBLIME - ChatStyle.BubbleColorPreset.GRADIENT_SEA -> ChatColorsPalette.Bubbles.SEA - ChatStyle.BubbleColorPreset.GRADIENT_TANGERINE -> ChatColorsPalette.Bubbles.TANGERINE - ChatStyle.BubbleColorPreset.UNKNOWN_BUBBLE_COLOR_PRESET, ChatStyle.BubbleColorPreset.SOLID_ULTRAMARINE -> ChatColorsPalette.Bubbles.ULTRAMARINE - } - } - - if (this.autoBubbleColor != null) { - return ChatColorsPalette.Bubbles.default.withId(ChatColors.Id.Auto) - } - - if (this.customColorId != null) { - return importState.remoteToLocalColorId[this.customColorId]?.let { localId -> - val colorId = ChatColors.Id.forLongValue(localId) - ChatColorsPalette.Bubbles.default.withId(colorId) - } - } - - return null -} - -private fun ChatColors.localToRemoteChatColors(): ChatStyle.BubbleColorPreset? { - when (this) { - // Solids - ChatColorsPalette.Bubbles.CRIMSON -> return ChatStyle.BubbleColorPreset.SOLID_CRIMSON - ChatColorsPalette.Bubbles.VERMILION -> return ChatStyle.BubbleColorPreset.SOLID_VERMILION - ChatColorsPalette.Bubbles.BURLAP -> return ChatStyle.BubbleColorPreset.SOLID_BURLAP - ChatColorsPalette.Bubbles.FOREST -> return ChatStyle.BubbleColorPreset.SOLID_FOREST - ChatColorsPalette.Bubbles.WINTERGREEN -> return ChatStyle.BubbleColorPreset.SOLID_WINTERGREEN - ChatColorsPalette.Bubbles.TEAL -> return ChatStyle.BubbleColorPreset.SOLID_TEAL - ChatColorsPalette.Bubbles.BLUE -> return ChatStyle.BubbleColorPreset.SOLID_BLUE - ChatColorsPalette.Bubbles.INDIGO -> return ChatStyle.BubbleColorPreset.SOLID_INDIGO - ChatColorsPalette.Bubbles.VIOLET -> return ChatStyle.BubbleColorPreset.SOLID_VIOLET - ChatColorsPalette.Bubbles.PLUM -> return ChatStyle.BubbleColorPreset.SOLID_PLUM - ChatColorsPalette.Bubbles.TAUPE -> return ChatStyle.BubbleColorPreset.SOLID_TAUPE - ChatColorsPalette.Bubbles.STEEL -> return ChatStyle.BubbleColorPreset.SOLID_STEEL - // Gradients - ChatColorsPalette.Bubbles.EMBER -> return ChatStyle.BubbleColorPreset.GRADIENT_EMBER - ChatColorsPalette.Bubbles.MIDNIGHT -> return ChatStyle.BubbleColorPreset.GRADIENT_MIDNIGHT - ChatColorsPalette.Bubbles.INFRARED -> return ChatStyle.BubbleColorPreset.GRADIENT_INFRARED - ChatColorsPalette.Bubbles.LAGOON -> return ChatStyle.BubbleColorPreset.GRADIENT_LAGOON - ChatColorsPalette.Bubbles.FLUORESCENT -> return ChatStyle.BubbleColorPreset.GRADIENT_FLUORESCENT - ChatColorsPalette.Bubbles.BASIL -> return ChatStyle.BubbleColorPreset.GRADIENT_BASIL - ChatColorsPalette.Bubbles.SUBLIME -> return ChatStyle.BubbleColorPreset.GRADIENT_SUBLIME - ChatColorsPalette.Bubbles.SEA -> return ChatStyle.BubbleColorPreset.GRADIENT_SEA - ChatColorsPalette.Bubbles.TANGERINE -> return ChatStyle.BubbleColorPreset.GRADIENT_TANGERINE - } - return null -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataProcessor.kt index 4199693aa2..dc689d9ed3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/processor/AccountDataProcessor.kt @@ -14,6 +14,8 @@ import org.thoughtcrime.securesms.backup.v2.proto.AccountData import org.thoughtcrime.securesms.backup.v2.proto.ChatStyle import org.thoughtcrime.securesms.backup.v2.proto.Frame import org.thoughtcrime.securesms.backup.v2.stream.BackupFrameEmitter +import org.thoughtcrime.securesms.backup.v2.util.BackupConverters +import org.thoughtcrime.securesms.backup.v2.util.toLocal import org.thoughtcrime.securesms.components.settings.app.subscription.InAppPaymentsRepository import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme import org.thoughtcrime.securesms.conversation.colors.ChatColors @@ -47,6 +49,8 @@ object AccountDataProcessor { val donationCurrency = signalStore.inAppPaymentValues.getSubscriptionCurrency(InAppPaymentSubscriberRecord.Type.DONATION) val donationSubscriber = db.inAppPaymentSubscriberTable.getByCurrencyCode(donationCurrency.currencyCode, InAppPaymentSubscriberRecord.Type.DONATION) + val chatColors = SignalStore.chatColors.chatColors + emitter.emit( Frame( account = AccountData( @@ -82,7 +86,13 @@ object AccountDataProcessor { displayBadgesOnProfile = signalStore.inAppPaymentValues.getDisplayBadgesOnProfile(), hasSeenGroupStoryEducationSheet = signalStore.storyValues.userHasSeenGroupStoryEducationSheet, hasCompletedUsernameOnboarding = signalStore.uiHintValues.hasCompletedUsernameOnboarding(), - customChatColors = db.chatColorsTable.getSavedChatColors().toRemoteChatColors() + customChatColors = db.chatColorsTable.getSavedChatColors().toRemoteChatColors(), + defaultChatStyle = BackupConverters.constructRemoteChatStyle(chatColors, chatColors?.id ?: ChatColors.Id.NotSet)?.also { + it.newBuilder().apply { + // TODO [backup] We should do this elsewhere once we handle wallpaper better + dimWallpaperInDarkMode = (SignalStore.wallpaper.wallpaper?.dimLevelForDarkTheme ?: 0f) > 0f + }.build() + } ), donationSubscriberData = donationSubscriber?.toSubscriberData(signalStore.inAppPaymentValues.isDonationSubscriptionManuallyCancelled()) ) @@ -142,6 +152,16 @@ object AccountDataProcessor { importState.remoteToLocalColorId[chatColor.id.longValue] = saved.id.longValue } + if (settings.defaultChatStyle != null) { + val chatColors = settings.defaultChatStyle.toLocal(importState) + SignalStore.chatColors.chatColors = chatColors + if (SignalStore.wallpaper.wallpaper != null) { + SignalStore.wallpaper.setDimInDarkTheme(settings.defaultChatStyle.dimWallpaperInDarkMode) + } + + // TODO [backup] wallpaper + } + if (accountData.donationSubscriberData != null) { if (accountData.donationSubscriberData.subscriberId.size > 0) { val remoteSubscriberId = SubscriberId.fromBytes(accountData.donationSubscriberData.subscriberId.toByteArray()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ModelConverters.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ModelConverters.kt new file mode 100644 index 0000000000..ced7dc638b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/util/ModelConverters.kt @@ -0,0 +1,112 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.backup.v2.util + +import org.thoughtcrime.securesms.backup.v2.ImportState +import org.thoughtcrime.securesms.backup.v2.proto.ChatStyle +import org.thoughtcrime.securesms.conversation.colors.ChatColors +import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette + +// TODO [backup] Passing in chatColorId probably unnecessary. Only stored as separate column in recipient table for querying, I believe. +object BackupConverters { + fun constructRemoteChatStyle(chatColors: ChatColors?, chatColorId: ChatColors.Id): ChatStyle? { + var chatStyleBuilder: ChatStyle.Builder? = null + + if (chatColors != null) { + chatStyleBuilder = ChatStyle.Builder() + when (chatColorId) { + ChatColors.Id.NotSet -> {} + ChatColors.Id.Auto -> { + chatStyleBuilder.autoBubbleColor = ChatStyle.AutomaticBubbleColor() + } + + ChatColors.Id.BuiltIn -> { + chatStyleBuilder.bubbleColorPreset = chatColors.toRemote() + } + + is ChatColors.Id.Custom -> { + chatStyleBuilder.customColorId = chatColorId.longValue + } + } + } + + // TODO [backup] wallpaper + + return chatStyleBuilder?.build() + } +} + +fun ChatStyle.toLocal(importState: ImportState): ChatColors? { + if (this.bubbleColorPreset != null) { + return when (this.bubbleColorPreset) { + ChatStyle.BubbleColorPreset.SOLID_CRIMSON -> ChatColorsPalette.Bubbles.CRIMSON + ChatStyle.BubbleColorPreset.SOLID_VERMILION -> ChatColorsPalette.Bubbles.VERMILION + ChatStyle.BubbleColorPreset.SOLID_BURLAP -> ChatColorsPalette.Bubbles.BURLAP + ChatStyle.BubbleColorPreset.SOLID_FOREST -> ChatColorsPalette.Bubbles.FOREST + ChatStyle.BubbleColorPreset.SOLID_WINTERGREEN -> ChatColorsPalette.Bubbles.WINTERGREEN + ChatStyle.BubbleColorPreset.SOLID_TEAL -> ChatColorsPalette.Bubbles.TEAL + ChatStyle.BubbleColorPreset.SOLID_BLUE -> ChatColorsPalette.Bubbles.BLUE + ChatStyle.BubbleColorPreset.SOLID_INDIGO -> ChatColorsPalette.Bubbles.INDIGO + ChatStyle.BubbleColorPreset.SOLID_VIOLET -> ChatColorsPalette.Bubbles.VIOLET + ChatStyle.BubbleColorPreset.SOLID_PLUM -> ChatColorsPalette.Bubbles.PLUM + ChatStyle.BubbleColorPreset.SOLID_TAUPE -> ChatColorsPalette.Bubbles.TAUPE + ChatStyle.BubbleColorPreset.SOLID_STEEL -> ChatColorsPalette.Bubbles.STEEL + ChatStyle.BubbleColorPreset.GRADIENT_EMBER -> ChatColorsPalette.Bubbles.EMBER + ChatStyle.BubbleColorPreset.GRADIENT_MIDNIGHT -> ChatColorsPalette.Bubbles.MIDNIGHT + ChatStyle.BubbleColorPreset.GRADIENT_INFRARED -> ChatColorsPalette.Bubbles.INFRARED + ChatStyle.BubbleColorPreset.GRADIENT_LAGOON -> ChatColorsPalette.Bubbles.LAGOON + ChatStyle.BubbleColorPreset.GRADIENT_FLUORESCENT -> ChatColorsPalette.Bubbles.FLUORESCENT + ChatStyle.BubbleColorPreset.GRADIENT_BASIL -> ChatColorsPalette.Bubbles.BASIL + ChatStyle.BubbleColorPreset.GRADIENT_SUBLIME -> ChatColorsPalette.Bubbles.SUBLIME + ChatStyle.BubbleColorPreset.GRADIENT_SEA -> ChatColorsPalette.Bubbles.SEA + ChatStyle.BubbleColorPreset.GRADIENT_TANGERINE -> ChatColorsPalette.Bubbles.TANGERINE + ChatStyle.BubbleColorPreset.UNKNOWN_BUBBLE_COLOR_PRESET, ChatStyle.BubbleColorPreset.SOLID_ULTRAMARINE -> ChatColorsPalette.Bubbles.ULTRAMARINE + } + } + + if (this.autoBubbleColor != null) { + return ChatColorsPalette.Bubbles.default.withId(ChatColors.Id.Auto) + } + + if (this.customColorId != null) { + return importState.remoteToLocalColorId[this.customColorId]?.let { localId -> + val colorId = ChatColors.Id.forLongValue(localId) + ChatColorsPalette.Bubbles.default.withId(colorId) + } + } + + return null +} + +fun ChatColors.toRemote(): ChatStyle.BubbleColorPreset? { + when (this) { + // Solids + ChatColorsPalette.Bubbles.CRIMSON -> return ChatStyle.BubbleColorPreset.SOLID_CRIMSON + ChatColorsPalette.Bubbles.VERMILION -> return ChatStyle.BubbleColorPreset.SOLID_VERMILION + ChatColorsPalette.Bubbles.BURLAP -> return ChatStyle.BubbleColorPreset.SOLID_BURLAP + ChatColorsPalette.Bubbles.FOREST -> return ChatStyle.BubbleColorPreset.SOLID_FOREST + ChatColorsPalette.Bubbles.WINTERGREEN -> return ChatStyle.BubbleColorPreset.SOLID_WINTERGREEN + ChatColorsPalette.Bubbles.TEAL -> return ChatStyle.BubbleColorPreset.SOLID_TEAL + ChatColorsPalette.Bubbles.BLUE -> return ChatStyle.BubbleColorPreset.SOLID_BLUE + ChatColorsPalette.Bubbles.INDIGO -> return ChatStyle.BubbleColorPreset.SOLID_INDIGO + ChatColorsPalette.Bubbles.VIOLET -> return ChatStyle.BubbleColorPreset.SOLID_VIOLET + ChatColorsPalette.Bubbles.PLUM -> return ChatStyle.BubbleColorPreset.SOLID_PLUM + ChatColorsPalette.Bubbles.TAUPE -> return ChatStyle.BubbleColorPreset.SOLID_TAUPE + ChatColorsPalette.Bubbles.STEEL -> return ChatStyle.BubbleColorPreset.SOLID_STEEL + ChatColorsPalette.Bubbles.ULTRAMARINE -> return ChatStyle.BubbleColorPreset.SOLID_ULTRAMARINE + // Gradients + ChatColorsPalette.Bubbles.EMBER -> return ChatStyle.BubbleColorPreset.GRADIENT_EMBER + ChatColorsPalette.Bubbles.MIDNIGHT -> return ChatStyle.BubbleColorPreset.GRADIENT_MIDNIGHT + ChatColorsPalette.Bubbles.INFRARED -> return ChatStyle.BubbleColorPreset.GRADIENT_INFRARED + ChatColorsPalette.Bubbles.LAGOON -> return ChatStyle.BubbleColorPreset.GRADIENT_LAGOON + ChatColorsPalette.Bubbles.FLUORESCENT -> return ChatStyle.BubbleColorPreset.GRADIENT_FLUORESCENT + ChatColorsPalette.Bubbles.BASIL -> return ChatStyle.BubbleColorPreset.GRADIENT_BASIL + ChatColorsPalette.Bubbles.SUBLIME -> return ChatStyle.BubbleColorPreset.GRADIENT_SUBLIME + ChatColorsPalette.Bubbles.SEA -> return ChatStyle.BubbleColorPreset.GRADIENT_SEA + ChatColorsPalette.Bubbles.TANGERINE -> return ChatStyle.BubbleColorPreset.GRADIENT_TANGERINE + } + return null +}