diff --git a/app/main.main.ts b/app/main.main.ts index da85a29df2..503ffa5766 100644 --- a/app/main.main.ts +++ b/app/main.main.ts @@ -45,7 +45,7 @@ import { createSupportUrl } from '../ts/util/createSupportUrl.std.js'; import { missingCaseError } from '../ts/util/missingCaseError.std.js'; import { strictAssert } from '../ts/util/assert.std.js'; import { drop } from '../ts/util/drop.std.js'; -import type { ThemeSettingType } from '../ts/types/StorageUIKeys.std.js'; +import type { ThemeSettingType } from '../ts/util/theme.std.js'; import { ThemeType } from '../ts/types/Util.std.js'; import { NotificationType } from '../ts/types/notifications.std.js'; import * as Errors from '../ts/types/errors.std.js'; diff --git a/ts/background.preload.ts b/ts/background.preload.ts index eebfb214f0..008a08f7bf 100644 --- a/ts/background.preload.ts +++ b/ts/background.preload.ts @@ -3395,18 +3395,6 @@ export async function startApp(): Promise { void Registration.remove(); - const NUMBER_ID_KEY = 'number_id'; - const UUID_ID_KEY = 'uuid_id'; - const PNI_KEY = 'pni'; - const LAST_PROCESSED_INDEX_KEY = 'attachmentMigration_lastProcessedIndex'; - const IS_MIGRATION_COMPLETE_KEY = 'attachmentMigration_isComplete'; - - const previousNumberId = itemStorage.get(NUMBER_ID_KEY); - const previousUuidId = itemStorage.get(UUID_ID_KEY); - const previousPni = itemStorage.get(PNI_KEY); - const lastProcessedIndex = itemStorage.get(LAST_PROCESSED_INDEX_KEY); - const isMigrationComplete = itemStorage.get(IS_MIGRATION_COMPLETE_KEY); - try { log.info('unlinkAndDisconnect: removing configuration'); @@ -3429,30 +3417,6 @@ export async function startApp(): Promise { // Finally, conversations in the database, and delete all config tables await signalProtocolStore.removeAllConfiguration(); - // These three bits of data are important to ensure that the app loads up - // the conversation list, instead of showing just the QR code screen. - if (previousNumberId !== undefined) { - await itemStorage.put(NUMBER_ID_KEY, previousNumberId); - } - if (previousUuidId !== undefined) { - await itemStorage.put(UUID_ID_KEY, previousUuidId); - } - if (previousPni !== undefined) { - await itemStorage.put(PNI_KEY, previousPni); - } - - // These two are important to ensure we don't rip through every message - // in the database attempting to upgrade it after starting up again. - await itemStorage.put( - IS_MIGRATION_COMPLETE_KEY, - isMigrationComplete || false - ); - if (lastProcessedIndex !== undefined) { - await itemStorage.put(LAST_PROCESSED_INDEX_KEY, lastProcessedIndex); - } else { - await itemStorage.remove(LAST_PROCESSED_INDEX_KEY); - } - // Re-hydrate items from memory; removeAllConfiguration above changed database await itemStorage.fetch(); @@ -3464,8 +3428,6 @@ export async function startApp(): Promise { Errors.toLogFormat(eraseError) ); } finally { - await Registration.markEverDone(); - if (window.SignalCI) { window.SignalCI.handleEvent('unlinkCleanupComplete', null); } diff --git a/ts/components/Preferences.dom.tsx b/ts/components/Preferences.dom.tsx index f5d2eafaec..d153d2327d 100644 --- a/ts/components/Preferences.dom.tsx +++ b/ts/components/Preferences.dom.tsx @@ -63,8 +63,8 @@ import type { NotificationSettingType, SentMediaQualitySettingType, ZoomFactorType, -} from '../types/Storage.d.ts'; -import type { ThemeSettingType } from '../types/StorageUIKeys.std.js'; +} from '../types/StorageKeys.std.js'; +import type { ThemeSettingType } from '../util/theme.std.js'; import type { AnyToast } from '../types/Toast.dom.js'; import { ToastType } from '../types/Toast.dom.js'; import type { ConversationType } from '../state/ducks/conversations.preload.js'; diff --git a/ts/components/fun/data/emojis.std.ts b/ts/components/fun/data/emojis.std.ts index 7880cb5ae7..7c837b63b5 100644 --- a/ts/components/fun/data/emojis.std.ts +++ b/ts/components/fun/data/emojis.std.ts @@ -11,6 +11,9 @@ import type { import type { FunEmojiLocalizerIndex } from '../useFunEmojiLocalizer.dom.js'; import { removeDiacritics } from '../../../util/removeDiacritics.std.js'; import { createLogger } from '../../../logging/log.std.js'; +import { EmojiSkinTone } from '../../../types/emoji.std.js'; + +export { EmojiSkinTone } from '../../../types/emoji.std.js'; const log = createLogger('fun/data/emojis'); @@ -46,15 +49,6 @@ export enum EmojiPickerCategory { Flags = 'EmojiPickerCategory.Flags', } -export enum EmojiSkinTone { - None = 'EmojiSkinTone.None', - Type1 = 'EmojiSkinTone.Type1', // 1F3FB - Type2 = 'EmojiSkinTone.Type2', // 1F3FC - Type3 = 'EmojiSkinTone.Type3', // 1F3FD - Type4 = 'EmojiSkinTone.Type4', // 1F3FE - Type5 = 'EmojiSkinTone.Type5', // 1F3FF -} - export function isValidEmojiSkinTone(value: unknown): value is EmojiSkinTone { return ( typeof value === 'string' && diff --git a/ts/sql/Server.node.ts b/ts/sql/Server.node.ts index 104c3633f9..bdea648451 100644 --- a/ts/sql/Server.node.ts +++ b/ts/sql/Server.node.ts @@ -32,7 +32,7 @@ import type { ReactionType } from '../types/Reactions.std.js'; import { ReactionReadStatus } from '../types/Reactions.std.js'; import type { AciString, ServiceIdString } from '../types/ServiceId.std.js'; import { isServiceIdString } from '../types/ServiceId.std.js'; -import { STORAGE_UI_KEYS } from '../types/StorageUIKeys.std.js'; +import { STORAGE_KEYS_TO_PRESERVE_AFTER_UNLINK } from '../types/StorageKeys.std.js'; import type { StoryDistributionIdString } from '../types/StoryDistributionId.std.js'; import * as Errors from '../types/errors.std.js'; import { assertDev, strictAssert } from '../util/assert.std.js'; @@ -8606,7 +8606,7 @@ function removeAllConfiguration(db: WritableDB): void { }) .all(); - const allowedSet = new Set(STORAGE_UI_KEYS); + const allowedSet = new Set(STORAGE_KEYS_TO_PRESERVE_AFTER_UNLINK); for (const id of itemIds) { if (!allowedSet.has(id)) { removeById(db, 'items', id); diff --git a/ts/state/smart/Preferences.preload.tsx b/ts/state/smart/Preferences.preload.tsx index 218cef7775..7fcb5ca768 100644 --- a/ts/state/smart/Preferences.preload.tsx +++ b/ts/state/smart/Preferences.preload.tsx @@ -102,10 +102,7 @@ import { } from './PreferencesNotificationProfiles.preload.js'; import type { SettingsLocation } from '../../types/Nav.std.js'; -import type { - StorageAccessType, - ZoomFactorType, -} from '../../types/Storage.d.ts'; +import type { StorageAccessType } from '../../types/Storage.d.ts'; import type { ThemeType } from '../../util/preload.preload.js'; import type { WidthBreakpoint } from '../../components/_util.std.js'; import { DialogType } from '../../types/Dialogs.std.js'; @@ -121,6 +118,7 @@ import type { SmartPreferencesChatFoldersPageProps } from './PreferencesChatFold import type { SmartPreferencesEditChatFolderPageProps } from './PreferencesEditChatFolderPage.preload.js'; import type { ExternalProps as SmartNotificationProfilesProps } from './PreferencesNotificationProfiles.preload.js'; import { useMegaphonesActions } from '../ducks/megaphones.preload.js'; +import type { ZoomFactorType } from '../../types/StorageKeys.std.js'; const DEFAULT_NOTIFICATION_SETTING = 'message'; @@ -606,7 +604,8 @@ export function SmartPreferences(): React.JSX.Element | null { defaultValue: StorageAccessType[K], callback?: (value: StorageAccessType[K]) => void ): [StorageAccessType[K], (value: StorageAccessType[K]) => void] { - const value = items[key] ?? defaultValue; + const value = + (items[key] as StorageAccessType[K] | undefined) ?? defaultValue; const setter = (newValue: StorageAccessType[K]) => { putItem(key, newValue); callback?.(newValue); diff --git a/ts/test-electron/sql/removeAllConfiguration_test.preload.ts b/ts/test-electron/sql/removeAllConfiguration_test.preload.ts index c637e4f81a..2924d8bc24 100644 --- a/ts/test-electron/sql/removeAllConfiguration_test.preload.ts +++ b/ts/test-electron/sql/removeAllConfiguration_test.preload.ts @@ -42,4 +42,40 @@ describe('Remove all configuration test', () => { 'Name (and all other fields) should be preserved' ); }); + + it('Removes non-preserved storage items', async () => { + /** Should be preserved */ + await DataWriter.createOrUpdateItem({ + id: 'zoomFactor', + value: 1.5, + }); + await DataWriter.createOrUpdateItem({ + id: 'version', + value: 'v1.2.3', + }); + await DataWriter.createOrUpdateItem({ + id: 'uuid_id', + value: 'aci-should-be-retained', + }); + + /** Should be deleted */ + await DataWriter.createOrUpdateItem({ + id: 'storageFetchComplete', + value: true, + }); + await DataWriter.createOrUpdateItem({ + // @ts-expect-error incorrect key + id: 'unknown-key', + value: 1.5, + }); + + await DataWriter.removeAllConfiguration(); + + const allItems = await DataReader.getAllItems(); + assert.deepStrictEqual(allItems, { + uuid_id: 'aci-should-be-retained', + version: 'v1.2.3', + zoomFactor: 1.5, + }); + }); }); diff --git a/ts/textsecure/AccountManager.preload.ts b/ts/textsecure/AccountManager.preload.ts index 0bafe678ad..f28b8839cc 100644 --- a/ts/textsecure/AccountManager.preload.ts +++ b/ts/textsecure/AccountManager.preload.ts @@ -86,6 +86,8 @@ import { signalProtocolStore } from '../SignalProtocolStore.preload.js'; import { itemStorage } from './Storage.preload.js'; import { deriveAccessKeyFromProfileKey } from '../util/zkgroup.node.js'; import { wrappingAdd24 } from '../util/wrappingAdd.std.js'; +import { everDone as registrationEverDone } from '../util/registration.preload.js'; +import { isAciString } from '../util/isAciString.std.js'; const { isNumber, omit, orderBy } = lodash; @@ -1033,8 +1035,20 @@ export default class AccountManager extends EventTarget { const numberChanged = !previousACI && previousNumber && previousNumber !== number; - let cleanStart = !previousACI && !previousPNI && !previousNumber; - if (uuidChanged || numberChanged) { + let cleanStart = + !previousACI && + !previousPNI && + !previousNumber && + !registrationEverDone(); + + // To be extra safe, clear everything if we know registration happened but there's no + // existing identifier + const hadPreviousIdentifier = + isAciString(previousACI) || Boolean(previousNumber); + const missingCriticalData = + registrationEverDone() && !hadPreviousIdentifier; + + if (uuidChanged || numberChanged || missingCriticalData) { if (uuidChanged) { log.warn( 'createAccount: New uuid is different from old uuid; deleting all previous data' @@ -1045,6 +1059,11 @@ export default class AccountManager extends EventTarget { 'createAccount: New number is different from old number; deleting all previous data' ); } + if (missingCriticalData) { + log.error( + 'createAccount: device had been registered but had no previous identifier' + ); + } try { await signalProtocolStore.removeAllData(); diff --git a/ts/types/RendererConfig.std.ts b/ts/types/RendererConfig.std.ts index 5cd7eb2b54..b086af96b9 100644 --- a/ts/types/RendererConfig.std.ts +++ b/ts/types/RendererConfig.std.ts @@ -4,9 +4,9 @@ import { z } from 'zod'; import { Environment } from '../environment.std.js'; -import { themeSettingSchema } from './StorageUIKeys.std.js'; import { HourCyclePreferenceSchema } from './I18N.std.js'; import { DNSFallbackSchema } from './DNSFallback.std.js'; +import { themeSettingSchema } from '../util/theme.std.js'; const environmentSchema = z.nativeEnum(Environment); diff --git a/ts/types/Storage.d.ts b/ts/types/Storage.d.ts index a77949baf5..f860d45f1d 100644 --- a/ts/types/Storage.d.ts +++ b/ts/types/Storage.d.ts @@ -1,315 +1,9 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only -import type { AudioDevice } from '@signalapp/ringrtc'; -import type { - CustomColorsItemType, - DefaultConversationColorType, -} from './Colors.std.js'; -import type { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability.std.js'; -import type { RetryItemType } from '../services/retryPlaceholders.std.js'; -import type { ConfigMapType as RemoteConfigType } from '../RemoteConfig.dom.js'; -import type { ExtendedStorageID, UnknownRecord } from './StorageService.d.ts'; +import type { StorageAccessType } from './StorageKeys.std.js'; -import type { GroupCredentialType } from '../textsecure/WebAPI.preload.js'; -import type { - SessionResetsType, - StorageServiceCredentials, -} from '../textsecure/Types.d.ts'; -import type { - BackupCredentialWrapperType, - BackupsSubscriptionType, - BackupStatusType, -} from './backups.node.js'; -import type { ServiceIdString } from './ServiceId.std.js'; -import type { RegisteredChallengeType } from '../challenge.dom.js'; -import type { ServerAlertsType } from '../util/handleServerAlerts.preload.js'; -import type { NotificationProfileOverride } from './NotificationProfile.std.js'; -import type { PhoneNumberSharingMode } from './PhoneNumberSharingMode.std.js'; -import type { LocalBackupExportMetadata } from './LocalExport.std.js'; - -export type AutoDownloadAttachmentType = { - photos: boolean; - videos: boolean; - audio: boolean; - documents: boolean; -}; - -export type SerializedCertificateType = { - expires: number; - serialized: Uint8Array; -}; - -export type ZoomFactorType = 0.75 | 1 | 1.25 | 1.5 | 2 | number; - -export type SentMediaQualitySettingType = 'standard' | 'high'; - -export type NotificationSettingType = 'message' | 'name' | 'count' | 'off'; - -export type IdentityKeyMap = Record< - ServiceIdString, - { - privKey: Uint8Array; - pubKey: Uint8Array; - } ->; - -// This should be in sync with `STORAGE_UI_KEYS` in `ts/types/StorageUIKeys.ts`. - -export type StorageAccessType = { - 'always-relay-calls': boolean; - 'audio-notification': boolean; - 'auto-download-update': boolean; - 'auto-download-attachment': AutoDownloadAttachmentType; - autoConvertEmoji: boolean; - 'badge-count-muted-conversations': boolean; - 'blocked-groups': ReadonlyArray; - 'blocked-uuids': ReadonlyArray; - 'call-ringtone-notification': boolean; - 'call-system-notification': boolean; - lastCallQualitySurveyTime: number; - lastCallQualityFailureSurveyTime: number; - cqsTestMode: boolean; - 'hide-menu-bar': boolean; - 'incoming-call-notification': boolean; - 'notification-draw-attention': boolean; - 'notification-setting': NotificationSettingType; - 'read-receipt-setting': boolean; - 'sent-media-quality': SentMediaQualitySettingType; - audioMessage: boolean; - attachmentMigration_isComplete: boolean; - attachmentMigration_lastProcessedIndex: number; - blocked: ReadonlyArray; - defaultConversationColor: DefaultConversationColorType; - - customColors: CustomColorsItemType; - device_name: string; - deviceCreatedAt: number; - existingOnboardingStoryMessageIds: ReadonlyArray | undefined; - hasSetMyStoriesPrivacy: boolean; - hasCompletedUsernameOnboarding: boolean; - hasCompletedUsernameLinkOnboarding: boolean; - hasCompletedSafetyNumberOnboarding: boolean; - hasSeenGroupStoryEducationSheet: boolean; - hasSeenNotificationProfileOnboarding: boolean; - hasSeenKeyTransparencyOnboarding: boolean; - hasViewedOnboardingStory: boolean; - hasStoriesDisabled: boolean; - hasKeyTransparencyDisabled: boolean; - storyViewReceiptsEnabled: boolean | undefined; - identityKeyMap: IdentityKeyMap; - lastAttemptedToRefreshProfilesAt: number; - lastResortKeyUpdateTime: number; - lastResortKeyUpdateTimePNI: number; - accountEntropyPool: string; - masterKey: string; - - accountEntropyPoolLastRequestTime: number; - maxPreKeyId: number; - maxPreKeyIdPNI: number; - maxKyberPreKeyId: number; - maxKyberPreKeyIdPNI: number; - number_id: string; - password: string; - profileKey: Uint8Array; - regionCode: string; - registrationIdMap: Record; - remoteBuildExpiration: number; - sessionResets: SessionResetsType; - showStickerPickerHint: boolean; - showStickersIntroduction: boolean; - seenPinMessageDisappearingMessagesWarningCount: number; - hasSeenAdminDeleteEducationDialog: boolean; - signedKeyId: number; - signedKeyIdPNI: number; - signedKeyUpdateTime: number; - signedKeyUpdateTimePNI: number; - storageKey: string; - synced_at: number | undefined; - userAgent: string; - uuid_id: string; - useRingrtcAdm: boolean; - pni: string; - version: string; - linkPreviews: boolean; - universalExpireTimer: number; - retryPlaceholders: ReadonlyArray; - donationWorkflow: string; - chromiumRegistrationDoneEver: ''; - chromiumRegistrationDone: ''; - phoneNumberSharingMode: PhoneNumberSharingMode; - phoneNumberDiscoverability: PhoneNumberDiscoverability; - pinnedConversationIds: ReadonlyArray; - preferContactAvatars: boolean; - textFormatting: boolean; - typingIndicators: boolean; - sealedSenderIndicators: boolean; - storageFetchComplete: boolean; - avatarUrl: string | undefined; - manifestVersion: number; - manifestRecordIkm: Uint8Array; - storageCredentials: StorageServiceCredentials; - 'storage-service-error-records': ReadonlyArray; - 'storage-service-unknown-records': ReadonlyArray; - 'storage-service-pending-deletes': ReadonlyArray; - 'preferred-video-input-device': string | undefined; - 'preferred-audio-input-device': AudioDevice | undefined; - 'preferred-audio-output-device': AudioDevice | undefined; - remoteConfig: RemoteConfigType; - remoteConfigHash: string; - serverTimeSkew: number; - unidentifiedDeliveryIndicators: boolean; - groupCredentials: ReadonlyArray; - callLinkAuthCredentials: ReadonlyArray; - backupCombinedCredentials: ReadonlyArray; - backupCombinedCredentialsLastRequestTime: number; - backupMediaRootKey: Uint8Array; - backupMediaDownloadTotalBytes: number; - backupMediaDownloadCompletedBytes: number; - backupMediaDownloadPaused: boolean; - backupMediaDownloadBannerDismissed: boolean; - attachmentDownloadManagerIdled: boolean; - messageInsertTriggersDisabled: boolean; - setBackupMessagesSignatureKey: boolean; - setBackupMediaSignatureKey: boolean; - lastReceivedAtCounter: number; - preferredReactionEmoji: ReadonlyArray; - emojiSkinToneDefault: EmojiSkinToneDefault; - unreadCount: number; - 'challenge:conversations': ReadonlyArray; - - deviceNameEncrypted: boolean; - 'indexeddb-delete-needed': boolean; - senderCertificate: SerializedCertificateType; - senderCertificateNoE164: SerializedCertificateType; - paymentAddress: string; - zoomFactor: ZoomFactorType; - preferredLeftPaneWidth: number; - nextScheduledUpdateKeyTime: number; - navTabsCollapsed: boolean; - areWeASubscriber: boolean; - subscriberId: Uint8Array; - subscriberCurrencyCode: string; - // Note: for historical reasons, this has two l's - donorSubscriptionManuallyCancelled: boolean; - backupsSubscriberId: Uint8Array; - backupsSubscriberPurchaseToken: string; - backupsSubscriberOriginalTransactionId: string; - displayBadgesOnProfile: boolean; - keepMutedChatsArchived: boolean; - usernameLastIntegrityCheck: number; - usernameCorrupted: boolean; - usernameLinkCorrupted: boolean; - usernameLinkColor: number; - usernameLink: { - entropy: Uint8Array; - serverId: Uint8Array; - }; - serverAlerts: ServerAlertsType; - needOrphanedAttachmentCheck: boolean; - needProfileMovedModal: boolean; - notificationProfileOverride: NotificationProfileOverride | undefined; - notificationProfileOverrideFromPrimary: - | NotificationProfileOverride - | undefined; - notificationProfileSyncDisabled: boolean; - observedCapabilities: { - attachmentBackfill?: true; - - // Note: Upon capability deprecation - change the value type to `never` and - // remove it in `ts/background.ts` - deleteSync?: never; - ssre2?: never; - }; - releaseNotesNextFetchTime: number; - releaseNotesVersionWatermark: string; - releaseNotesPreviousManifestHash: string; - - // If present - we are downloading backup - backupDownloadPath: string; - - // If present together with backupDownloadPath - we are downloading - // link-and-sync backup - backupEphemeralKey: Uint8Array; - - // If present - we are resuming the download of known transfer archive - backupTransitArchive: { - cdn: number; - key: string; - }; - - backupTier: number | undefined; - cloudBackupStatus: BackupStatusType | undefined; - backupSubscriptionStatus: BackupsSubscriptionType | undefined; - - backupKeyViewed: boolean; - lastLocalBackup: LocalBackupExportMetadata; - localBackupFolder: string | undefined; - - // If true Desktop message history was restored from backup - isRestoredFromBackup: boolean; - - // The `firstAppVersion` present on an BackupInfo from an imported backup. - restoredBackupFirstAppVersion: string; - - // Stored solely for pesistance during import/export sequence - svrPin: string; - optimizeOnDeviceStorage: boolean; - pinReminders: boolean | undefined; - screenLockTimeoutMinutes: number | undefined; - 'auto-download-attachment-primary': - | undefined - | { - photos: number; - audio: number; - videos: number; - documents: number; - }; - androidSpecificSettings: unknown; - callsUseLessDataSetting: unknown; - allowSealedSenderFromAnyone: unknown; - - postRegistrationSyncsStatus: 'incomplete' | 'complete'; - - avatarsHaveBeenMigrated: boolean; - - // Key Transparency - lastDistinguishedTreeHead: Uint8Array; - // Meaning of values: - // - // - undefined - status unknown or uninitialized - // - 'ok' - last check passed - // - 'intermittent' - last check failed, but we haven't retried yet - // - 'fail' - last check failed after retry - keyTransparencySelfHealth: undefined | 'ok' | 'intermittent' | 'fail'; - lastKeyTransparencySelfCheck: number; - - // Test-only - // Not used UI, stored as is when imported from backup during tests - defaultWallpaperPhotoPointer: Uint8Array; - defaultWallpaperPreset: number; - defaultDimWallpaperInDarkMode: boolean; - defaultAutoBubbleColor: boolean; - - // Deprecated - 'challenge:retry-message-ids': never; - nextSignedKeyRotationTime: number; - previousAudioDeviceModule: never; - senderCertificateWithUuid: never; - signaling_key: never; - signedKeyRotationRejected: number; - lastHeartbeat: never; - lastStartup: never; - sendEditWarningShown: never; - formattingWarningShown: never; - hasRegisterSupportForUnauthenticatedDelivery: never; - masterKeyLastRequestTime: never; - versionedExpirationTimer: never; - primarySendsSms: never; - backupMediaDownloadIdle: never; - callQualitySurveyCooldownDisabled: never; - localDeleteWarningShown: never; -}; +export type { StorageAccessType } from './StorageKeys.std.js'; export type StorageInterface = { onready(callback: () => void): void; diff --git a/ts/types/StorageKeys.std.ts b/ts/types/StorageKeys.std.ts new file mode 100644 index 0000000000..356ffe2e81 --- /dev/null +++ b/ts/types/StorageKeys.std.ts @@ -0,0 +1,540 @@ +// Copyright 2026 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only + +import type { AudioDevice } from '@signalapp/ringrtc'; +import type { + CustomColorsItemType, + DefaultConversationColorType, +} from './Colors.std.js'; +import type { PhoneNumberDiscoverability } from '../util/phoneNumberDiscoverability.std.js'; +import type { RetryItemType } from '../services/retryPlaceholders.std.js'; +import type { ConfigMapType as RemoteConfigType } from '../RemoteConfig.dom.js'; +import type { ExtendedStorageID, UnknownRecord } from './StorageService.js'; + +import type { GroupCredentialType } from '../textsecure/WebAPI.preload.js'; +import type { + SessionResetsType, + StorageServiceCredentials, +} from '../textsecure/Types.js'; +import type { + BackupCredentialWrapperType, + BackupsSubscriptionType, + BackupStatusType, +} from './backups.node.js'; +import type { ServiceIdString } from './ServiceId.std.js'; +import type { RegisteredChallengeType } from '../challenge.dom.js'; +import type { NotificationProfileOverride } from './NotificationProfile.std.js'; +import type { PhoneNumberSharingMode } from './PhoneNumberSharingMode.std.js'; +import type { LocalBackupExportMetadata } from './LocalExport.std.js'; +import type { ServerAlertsType } from './ServerAlert.std.js'; +import type { EmojiSkinTone } from './emoji.std.js'; +import type { AssertSameMembers } from './Util.std.js'; + +export type AutoDownloadAttachmentType = { + photos: boolean; + videos: boolean; + audio: boolean; + documents: boolean; +}; + +export type SerializedCertificateType = { + expires: number; + serialized: Uint8Array; +}; + +export type ZoomFactorType = 0.75 | 1 | 1.25 | 1.5 | 2 | number; + +export type SentMediaQualitySettingType = 'standard' | 'high'; + +export type NotificationSettingType = 'message' | 'name' | 'count' | 'off'; + +export type IdentityKeyMap = Record< + ServiceIdString, + { + privKey: Uint8Array; + pubKey: Uint8Array; + } +>; + +export type StorageAccessType = { + 'always-relay-calls': boolean; + 'audio-notification': boolean; + 'auto-download-update': boolean; + 'auto-download-attachment': AutoDownloadAttachmentType; + autoConvertEmoji: boolean; + 'badge-count-muted-conversations': boolean; + 'blocked-groups': ReadonlyArray; + 'blocked-uuids': ReadonlyArray; + 'call-ringtone-notification': boolean; + 'call-system-notification': boolean; + lastCallQualitySurveyTime: number; + lastCallQualityFailureSurveyTime: number; + cqsTestMode: boolean; + 'hide-menu-bar': boolean; + 'incoming-call-notification': boolean; + 'notification-draw-attention': boolean; + 'notification-setting': NotificationSettingType; + 'read-receipt-setting': boolean; + 'sent-media-quality': SentMediaQualitySettingType; + audioMessage: boolean; + attachmentMigration_isComplete: boolean; + attachmentMigration_lastProcessedIndex: number; + blocked: ReadonlyArray; + defaultConversationColor: DefaultConversationColorType; + + customColors: CustomColorsItemType; + device_name: string; + deviceCreatedAt: number; + existingOnboardingStoryMessageIds: ReadonlyArray | undefined; + hasSetMyStoriesPrivacy: boolean; + hasCompletedUsernameOnboarding: boolean; + hasCompletedUsernameLinkOnboarding: boolean; + hasCompletedSafetyNumberOnboarding: boolean; + hasSeenGroupStoryEducationSheet: boolean; + hasSeenNotificationProfileOnboarding: boolean; + hasSeenKeyTransparencyOnboarding: boolean; + hasViewedOnboardingStory: boolean; + hasStoriesDisabled: boolean; + hasKeyTransparencyDisabled: boolean; + storyViewReceiptsEnabled: boolean | undefined; + identityKeyMap: IdentityKeyMap; + lastAttemptedToRefreshProfilesAt: number; + lastResortKeyUpdateTime: number; + lastResortKeyUpdateTimePNI: number; + accountEntropyPool: string; + masterKey: string; + + accountEntropyPoolLastRequestTime: number; + maxPreKeyId: number; + maxPreKeyIdPNI: number; + maxKyberPreKeyId: number; + maxKyberPreKeyIdPNI: number; + number_id: string; + password: string; + profileKey: Uint8Array; + regionCode: string; + registrationIdMap: Record; + remoteBuildExpiration: number; + sessionResets: SessionResetsType; + showStickerPickerHint: boolean; + showStickersIntroduction: boolean; + seenPinMessageDisappearingMessagesWarningCount: number; + hasSeenAdminDeleteEducationDialog: boolean; + signedKeyId: number; + signedKeyIdPNI: number; + signedKeyUpdateTime: number; + signedKeyUpdateTimePNI: number; + storageKey: string; + synced_at: number | undefined; + userAgent: string; + uuid_id: string; + useRingrtcAdm: boolean; + pni: string; + version: string; + linkPreviews: boolean; + universalExpireTimer: number; + retryPlaceholders: ReadonlyArray; + donationWorkflow: string; + chromiumRegistrationDoneEver: ''; + chromiumRegistrationDone: ''; + phoneNumberSharingMode: PhoneNumberSharingMode; + phoneNumberDiscoverability: PhoneNumberDiscoverability; + pinnedConversationIds: ReadonlyArray; + preferContactAvatars: boolean; + textFormatting: boolean; + typingIndicators: boolean; + sealedSenderIndicators: boolean; + storageFetchComplete: boolean; + avatarUrl: string | undefined; + manifestVersion: number; + manifestRecordIkm: Uint8Array; + storageCredentials: StorageServiceCredentials; + 'storage-service-error-records': ReadonlyArray; + 'storage-service-unknown-records': ReadonlyArray; + 'storage-service-pending-deletes': ReadonlyArray; + 'preferred-video-input-device': string | undefined; + 'preferred-audio-input-device': AudioDevice | undefined; + 'preferred-audio-output-device': AudioDevice | undefined; + remoteConfig: RemoteConfigType; + remoteConfigHash: string; + serverTimeSkew: number; + unidentifiedDeliveryIndicators: boolean; + groupCredentials: ReadonlyArray; + callLinkAuthCredentials: ReadonlyArray; + backupCombinedCredentials: ReadonlyArray; + backupCombinedCredentialsLastRequestTime: number; + backupMediaRootKey: Uint8Array; + backupMediaDownloadTotalBytes: number; + backupMediaDownloadCompletedBytes: number; + backupMediaDownloadPaused: boolean; + backupMediaDownloadBannerDismissed: boolean; + attachmentDownloadManagerIdled: boolean; + messageInsertTriggersDisabled: boolean; + setBackupMessagesSignatureKey: boolean; + setBackupMediaSignatureKey: boolean; + lastReceivedAtCounter: number; + preferredReactionEmoji: ReadonlyArray; + emojiSkinToneDefault: EmojiSkinTone; + unreadCount: number; + 'challenge:conversations': ReadonlyArray; + + deviceNameEncrypted: boolean; + 'indexeddb-delete-needed': boolean; + senderCertificate: SerializedCertificateType; + senderCertificateNoE164: SerializedCertificateType; + paymentAddress: string; + zoomFactor: ZoomFactorType; + preferredLeftPaneWidth: number; + nextScheduledUpdateKeyTime: number; + navTabsCollapsed: boolean; + areWeASubscriber: boolean; + subscriberId: Uint8Array; + subscriberCurrencyCode: string; + // Note: for historical reasons, this has two l's + donorSubscriptionManuallyCancelled: boolean; + backupsSubscriberId: Uint8Array; + backupsSubscriberPurchaseToken: string; + backupsSubscriberOriginalTransactionId: string; + displayBadgesOnProfile: boolean; + keepMutedChatsArchived: boolean; + usernameLastIntegrityCheck: number; + usernameCorrupted: boolean; + usernameLinkCorrupted: boolean; + usernameLinkColor: number; + usernameLink: { + entropy: Uint8Array; + serverId: Uint8Array; + }; + serverAlerts: ServerAlertsType; + needOrphanedAttachmentCheck: boolean; + needProfileMovedModal: boolean; + notificationProfileOverride: NotificationProfileOverride | undefined; + notificationProfileOverrideFromPrimary: + | NotificationProfileOverride + | undefined; + notificationProfileSyncDisabled: boolean; + observedCapabilities: { + attachmentBackfill?: true; + + // Note: Upon capability deprecation - change the value type to `never` and + // remove it in `ts/background.ts` + deleteSync?: never; + ssre2?: never; + }; + releaseNotesNextFetchTime: number; + releaseNotesVersionWatermark: string; + releaseNotesPreviousManifestHash: string; + + // If present - we are downloading backup + backupDownloadPath: string; + + // If present together with backupDownloadPath - we are downloading + // link-and-sync backup + backupEphemeralKey: Uint8Array; + + // If present - we are resuming the download of known transfer archive + backupTransitArchive: { + cdn: number; + key: string; + }; + + backupTier: number | undefined; + cloudBackupStatus: BackupStatusType | undefined; + backupSubscriptionStatus: BackupsSubscriptionType | undefined; + + backupKeyViewed: boolean; + lastLocalBackup: LocalBackupExportMetadata; + localBackupFolder: string | undefined; + + // If true Desktop message history was restored from backup + isRestoredFromBackup: boolean; + + // The `firstAppVersion` present on an BackupInfo from an imported backup. + restoredBackupFirstAppVersion: string; + + // Stored solely for pesistance during import/export sequence + svrPin: string; + optimizeOnDeviceStorage: boolean; + pinReminders: boolean | undefined; + screenLockTimeoutMinutes: number | undefined; + 'auto-download-attachment-primary': + | undefined + | { + photos: number; + audio: number; + videos: number; + documents: number; + }; + androidSpecificSettings: unknown; + callsUseLessDataSetting: unknown; + allowSealedSenderFromAnyone: unknown; + + postRegistrationSyncsStatus: 'incomplete' | 'complete'; + + avatarsHaveBeenMigrated: boolean; + + // Key Transparency + lastDistinguishedTreeHead: Uint8Array; + // Meaning of values: + // + // - undefined - status unknown or uninitialized + // - 'ok' - last check passed + // - 'intermittent' - last check failed, but we haven't retried yet + // - 'fail' - last check failed after retry + keyTransparencySelfHealth: undefined | 'ok' | 'intermittent' | 'fail'; + lastKeyTransparencySelfCheck: number; + + // Test-only + // Not used UI, stored as is when imported from backup during tests + defaultWallpaperPhotoPointer: Uint8Array; + defaultWallpaperPreset: number; + defaultDimWallpaperInDarkMode: boolean; + defaultAutoBubbleColor: boolean; + + // Deprecated + 'challenge:retry-message-ids': never; + nextSignedKeyRotationTime: number; + previousAudioDeviceModule: never; + senderCertificateWithUuid: never; + signaling_key: never; + signedKeyRotationRejected: number; + lastHeartbeat: never; + lastStartup: never; + sendEditWarningShown: never; + formattingWarningShown: never; + hasRegisterSupportForUnauthenticatedDelivery: never; + masterKeyLastRequestTime: never; + versionedExpirationTimer: never; + primarySendsSms: never; + backupMediaDownloadIdle: never; + callQualitySurveyCooldownDisabled: never; + localDeleteWarningShown: never; +}; + +export const STORAGE_KEYS_TO_PRESERVE_AFTER_UNLINK = [ + // UI & user-setting-related keys + 'always-relay-calls', + 'audio-notification', + 'audioMessage', + 'auto-download-update', + 'autoConvertEmoji', + 'badge-count-muted-conversations', + 'call-ringtone-notification', + 'call-system-notification', + 'customColors', + 'defaultConversationColor', + 'existingOnboardingStoryMessageIds', + 'hasCompletedSafetyNumberOnboarding', + 'hasCompletedUsernameLinkOnboarding', + 'hide-menu-bar', + 'incoming-call-notification', + 'navTabsCollapsed', + 'notification-draw-attention', + 'notification-setting', + 'pinnedConversationIds', + 'preferred-audio-input-device', + 'preferred-audio-output-device', + 'preferred-video-input-device', + 'preferredLeftPaneWidth', + 'preferredReactionEmoji', + 'sent-media-quality', + 'showStickerPickerHint', + 'showStickersIntroduction', + 'emojiSkinToneDefault', + 'textFormatting', + 'zoomFactor', + + // Bookkeeping keys + 'attachmentMigration_lastProcessedIndex', + 'attachmentMigration_isComplete', + 'chromiumRegistrationDoneEver', + 'version', + 'number_id', + 'uuid_id', + 'pni', +] as const satisfies ReadonlyArray; + +const STORAGE_KEYS_TO_REMOVE_AFTER_UNLINK = [ + 'auto-download-attachment', + 'blocked-groups', + 'blocked-uuids', + 'lastCallQualitySurveyTime', + 'lastCallQualityFailureSurveyTime', + 'cqsTestMode', + 'read-receipt-setting', + 'blocked', + 'device_name', + 'deviceCreatedAt', + 'hasSetMyStoriesPrivacy', + 'hasCompletedUsernameOnboarding', + 'hasSeenGroupStoryEducationSheet', + 'hasSeenNotificationProfileOnboarding', + 'hasSeenKeyTransparencyOnboarding', + 'hasViewedOnboardingStory', + 'hasStoriesDisabled', + 'hasKeyTransparencyDisabled', + 'storyViewReceiptsEnabled', + 'identityKeyMap', + 'lastAttemptedToRefreshProfilesAt', + 'lastResortKeyUpdateTime', + 'lastResortKeyUpdateTimePNI', + 'accountEntropyPool', + 'masterKey', + 'accountEntropyPoolLastRequestTime', + 'maxPreKeyId', + 'maxPreKeyIdPNI', + 'maxKyberPreKeyId', + 'maxKyberPreKeyIdPNI', + 'password', + 'profileKey', + 'regionCode', + 'registrationIdMap', + 'remoteBuildExpiration', + 'sessionResets', + 'seenPinMessageDisappearingMessagesWarningCount', + 'hasSeenAdminDeleteEducationDialog', + 'signedKeyId', + 'signedKeyIdPNI', + 'signedKeyUpdateTime', + 'signedKeyUpdateTimePNI', + 'storageKey', + 'synced_at', + 'userAgent', + 'useRingrtcAdm', + 'linkPreviews', + 'universalExpireTimer', + 'retryPlaceholders', + 'donationWorkflow', + 'chromiumRegistrationDone', + 'phoneNumberSharingMode', + 'phoneNumberDiscoverability', + 'preferContactAvatars', + 'typingIndicators', + 'sealedSenderIndicators', + 'storageFetchComplete', + 'avatarUrl', + 'manifestVersion', + 'manifestRecordIkm', + 'storageCredentials', + 'storage-service-error-records', + 'storage-service-unknown-records', + 'storage-service-pending-deletes', + 'remoteConfig', + 'remoteConfigHash', + 'serverTimeSkew', + 'unidentifiedDeliveryIndicators', + 'groupCredentials', + 'callLinkAuthCredentials', + 'backupCombinedCredentials', + 'backupCombinedCredentialsLastRequestTime', + 'backupMediaRootKey', + 'backupMediaDownloadTotalBytes', + 'backupMediaDownloadCompletedBytes', + 'backupMediaDownloadPaused', + 'backupMediaDownloadBannerDismissed', + 'attachmentDownloadManagerIdled', + 'messageInsertTriggersDisabled', + 'setBackupMessagesSignatureKey', + 'setBackupMediaSignatureKey', + 'lastReceivedAtCounter', + 'unreadCount', + 'challenge:conversations', + 'deviceNameEncrypted', + 'indexeddb-delete-needed', + 'senderCertificate', + 'senderCertificateNoE164', + 'paymentAddress', + 'nextScheduledUpdateKeyTime', + 'areWeASubscriber', + 'subscriberId', + 'subscriberCurrencyCode', + 'donorSubscriptionManuallyCancelled', + 'backupsSubscriberId', + 'backupsSubscriberPurchaseToken', + 'backupsSubscriberOriginalTransactionId', + 'displayBadgesOnProfile', + 'keepMutedChatsArchived', + 'usernameLastIntegrityCheck', + 'usernameCorrupted', + 'usernameLinkCorrupted', + 'usernameLinkColor', + 'usernameLink', + 'serverAlerts', + 'needOrphanedAttachmentCheck', + 'needProfileMovedModal', + 'notificationProfileOverride', + 'notificationProfileOverrideFromPrimary', + 'notificationProfileSyncDisabled', + 'observedCapabilities', + 'releaseNotesNextFetchTime', + 'releaseNotesVersionWatermark', + 'releaseNotesPreviousManifestHash', + 'backupDownloadPath', + 'backupEphemeralKey', + 'backupTransitArchive', + 'backupTier', + 'cloudBackupStatus', + 'backupSubscriptionStatus', + 'backupKeyViewed', + 'lastLocalBackup', + 'localBackupFolder', + 'isRestoredFromBackup', + 'restoredBackupFirstAppVersion', + 'svrPin', + 'optimizeOnDeviceStorage', + 'pinReminders', + 'screenLockTimeoutMinutes', + 'auto-download-attachment-primary', + 'androidSpecificSettings', + 'callsUseLessDataSetting', + 'allowSealedSenderFromAnyone', + 'postRegistrationSyncsStatus', + 'avatarsHaveBeenMigrated', + 'lastDistinguishedTreeHead', + 'keyTransparencySelfHealth', + 'lastKeyTransparencySelfCheck', + 'defaultWallpaperPhotoPointer', + 'defaultWallpaperPreset', + 'defaultDimWallpaperInDarkMode', + 'defaultAutoBubbleColor', + 'challenge:retry-message-ids', + 'nextSignedKeyRotationTime', + 'previousAudioDeviceModule', + 'senderCertificateWithUuid', + 'signaling_key', + 'signedKeyRotationRejected', + 'lastHeartbeat', + 'lastStartup', + 'sendEditWarningShown', + 'formattingWarningShown', + 'hasRegisterSupportForUnauthenticatedDelivery', + 'masterKeyLastRequestTime', + 'versionedExpirationTimer', + 'primarySendsSms', + 'backupMediaDownloadIdle', + 'callQualitySurveyCooldownDisabled', + 'localDeleteWarningShown', +] as const satisfies ReadonlyArray; + +// Ensure every storage key is explicitly marked to be preserved or removed on unlink. + +type AssertTrue = T; + +type StorageKeysToPreserveAfterUnlink = + (typeof STORAGE_KEYS_TO_PRESERVE_AFTER_UNLINK)[number]; +type StorageKeysToRemoveAfterUnlink = + (typeof STORAGE_KEYS_TO_REMOVE_AFTER_UNLINK)[number]; + +export type AssertStorageUnlinkKeysDoNotOverlap = AssertTrue< + AssertSameMembers< + Extract, + never + > +>; + +export type AssertStorageUnlinkKeysAreExhaustive = AssertTrue< + AssertSameMembers< + StorageKeysToPreserveAfterUnlink | StorageKeysToRemoveAfterUnlink, + keyof StorageAccessType + > +>; diff --git a/ts/types/StorageUIKeys.std.ts b/ts/types/StorageUIKeys.std.ts deleted file mode 100644 index dba5521239..0000000000 --- a/ts/types/StorageUIKeys.std.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2021 Signal Messenger, LLC -// SPDX-License-Identifier: AGPL-3.0-only - -import { z } from 'zod'; -import type { StorageAccessType } from './Storage.d.ts'; - -export const themeSettingSchema = z.enum(['system', 'light', 'dark']); -export type ThemeSettingType = z.infer; - -// Configuration keys that only affect UI -export const STORAGE_UI_KEYS: ReadonlyArray = [ - 'always-relay-calls', - 'audio-notification', - 'audioMessage', - 'auto-download-update', - 'autoConvertEmoji', - 'badge-count-muted-conversations', - 'call-ringtone-notification', - 'call-system-notification', - 'customColors', - 'defaultConversationColor', - 'existingOnboardingStoryMessageIds', - 'hasCompletedSafetyNumberOnboarding', - 'hasCompletedUsernameLinkOnboarding', - 'hide-menu-bar', - 'incoming-call-notification', - 'navTabsCollapsed', - 'notification-draw-attention', - 'notification-setting', - 'pinnedConversationIds', - 'preferred-audio-input-device', - 'preferred-audio-output-device', - 'preferred-video-input-device', - 'preferredLeftPaneWidth', - 'preferredReactionEmoji', - 'sent-media-quality', - 'showStickerPickerHint', - 'showStickersIntroduction', - 'emojiSkinToneDefault', - 'textFormatting', - 'version', - 'zoomFactor', -]; diff --git a/ts/types/emoji.std.ts b/ts/types/emoji.std.ts index 05c46eca73..5d94054899 100644 --- a/ts/types/emoji.std.ts +++ b/ts/types/emoji.std.ts @@ -15,3 +15,12 @@ export type LocaleEmojiType = z.infer; export const LocaleEmojiListSchema = LocaleEmojiSchema.array(); export type LocaleEmojiListType = z.infer; + +export enum EmojiSkinTone { + None = 'EmojiSkinTone.None', + Type1 = 'EmojiSkinTone.Type1', // 1F3FB + Type2 = 'EmojiSkinTone.Type2', // 1F3FC + Type3 = 'EmojiSkinTone.Type3', // 1F3FD + Type4 = 'EmojiSkinTone.Type4', // 1F3FE + Type5 = 'EmojiSkinTone.Type5', // 1F3FF +} diff --git a/ts/updater/common.main.ts b/ts/updater/common.main.ts index fe6fbd1e13..6f8448f348 100644 --- a/ts/updater/common.main.ts +++ b/ts/updater/common.main.ts @@ -943,7 +943,7 @@ export abstract class Updater { 'getItemById', 'auto-download-update' ); - return result?.value ?? true; + return typeof result?.value === 'boolean' ? result.value : true; } catch (error) { this.logger.warn( 'getAutoDownloadUpdateSetting: Failed to fetch, returning false', diff --git a/ts/util/createIPCEvents.preload.ts b/ts/util/createIPCEvents.preload.ts index 24fc3b5789..d6b298a21c 100644 --- a/ts/util/createIPCEvents.preload.ts +++ b/ts/util/createIPCEvents.preload.ts @@ -5,7 +5,7 @@ import { ipcRenderer } from 'electron'; import type { SystemPreferences } from 'electron'; import lodash from 'lodash'; -import type { ZoomFactorType } from '../types/Storage.d.ts'; +import type { ZoomFactorType } from '../types/StorageKeys.std.js'; import * as Errors from '../types/errors.std.js'; import * as Stickers from '../types/Stickers.preload.js'; import * as Settings from '../types/Settings.std.js'; diff --git a/ts/util/theme.std.ts b/ts/util/theme.std.ts index 9b3fdce219..87b1db79cd 100644 --- a/ts/util/theme.std.ts +++ b/ts/util/theme.std.ts @@ -1,6 +1,7 @@ // Copyright 2020 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only +import { z } from 'zod'; import { missingCaseError } from './missingCaseError.std.js'; import { ThemeType } from '../types/Util.std.js'; @@ -9,6 +10,9 @@ export enum Theme { Dark, } +export const themeSettingSchema = z.enum(['system', 'light', 'dark']); +export type ThemeSettingType = z.infer; + export function themeClassName(theme: Theme): string { switch (theme) { case Theme.Light: