Make explicit storage item preserve/remove behavior on unlink

This commit is contained in:
trevor-signal
2026-03-16 09:33:49 -07:00
committed by GitHub
parent e024df318e
commit 178e93924f
16 changed files with 627 additions and 413 deletions

View File

@@ -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';

View File

@@ -3395,18 +3395,6 @@ export async function startApp(): Promise<void> {
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<void> {
// 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<void> {
Errors.toLogFormat(eraseError)
);
} finally {
await Registration.markEverDone();
if (window.SignalCI) {
window.SignalCI.handleEvent('unlinkCleanupComplete', null);
}

View File

@@ -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';

View File

@@ -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' &&

View File

@@ -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<string>(STORAGE_UI_KEYS);
const allowedSet = new Set<string>(STORAGE_KEYS_TO_PRESERVE_AFTER_UNLINK);
for (const id of itemIds) {
if (!allowedSet.has(id)) {
removeById(db, 'items', id);

View File

@@ -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);

View File

@@ -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,
});
});
});

View File

@@ -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();

View File

@@ -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);

310
ts/types/Storage.d.ts vendored
View File

@@ -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<string>;
'blocked-uuids': ReadonlyArray<ServiceIdString>;
'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<string>;
defaultConversationColor: DefaultConversationColorType;
customColors: CustomColorsItemType;
device_name: string;
deviceCreatedAt: number;
existingOnboardingStoryMessageIds: ReadonlyArray<string> | 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<ServiceIdString, number>;
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<RetryItemType>;
donationWorkflow: string;
chromiumRegistrationDoneEver: '';
chromiumRegistrationDone: '';
phoneNumberSharingMode: PhoneNumberSharingMode;
phoneNumberDiscoverability: PhoneNumberDiscoverability;
pinnedConversationIds: ReadonlyArray<string>;
preferContactAvatars: boolean;
textFormatting: boolean;
typingIndicators: boolean;
sealedSenderIndicators: boolean;
storageFetchComplete: boolean;
avatarUrl: string | undefined;
manifestVersion: number;
manifestRecordIkm: Uint8Array;
storageCredentials: StorageServiceCredentials;
'storage-service-error-records': ReadonlyArray<UnknownRecord>;
'storage-service-unknown-records': ReadonlyArray<UnknownRecord>;
'storage-service-pending-deletes': ReadonlyArray<ExtendedStorageID>;
'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<GroupCredentialType>;
callLinkAuthCredentials: ReadonlyArray<GroupCredentialType>;
backupCombinedCredentials: ReadonlyArray<BackupCredentialWrapperType>;
backupCombinedCredentialsLastRequestTime: number;
backupMediaRootKey: Uint8Array;
backupMediaDownloadTotalBytes: number;
backupMediaDownloadCompletedBytes: number;
backupMediaDownloadPaused: boolean;
backupMediaDownloadBannerDismissed: boolean;
attachmentDownloadManagerIdled: boolean;
messageInsertTriggersDisabled: boolean;
setBackupMessagesSignatureKey: boolean;
setBackupMediaSignatureKey: boolean;
lastReceivedAtCounter: number;
preferredReactionEmoji: ReadonlyArray<string>;
emojiSkinToneDefault: EmojiSkinToneDefault;
unreadCount: number;
'challenge:conversations': ReadonlyArray<RegisteredChallengeType>;
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;

540
ts/types/StorageKeys.std.ts Normal file
View File

@@ -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<string>;
'blocked-uuids': ReadonlyArray<ServiceIdString>;
'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<string>;
defaultConversationColor: DefaultConversationColorType;
customColors: CustomColorsItemType;
device_name: string;
deviceCreatedAt: number;
existingOnboardingStoryMessageIds: ReadonlyArray<string> | 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<ServiceIdString, number>;
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<RetryItemType>;
donationWorkflow: string;
chromiumRegistrationDoneEver: '';
chromiumRegistrationDone: '';
phoneNumberSharingMode: PhoneNumberSharingMode;
phoneNumberDiscoverability: PhoneNumberDiscoverability;
pinnedConversationIds: ReadonlyArray<string>;
preferContactAvatars: boolean;
textFormatting: boolean;
typingIndicators: boolean;
sealedSenderIndicators: boolean;
storageFetchComplete: boolean;
avatarUrl: string | undefined;
manifestVersion: number;
manifestRecordIkm: Uint8Array;
storageCredentials: StorageServiceCredentials;
'storage-service-error-records': ReadonlyArray<UnknownRecord>;
'storage-service-unknown-records': ReadonlyArray<UnknownRecord>;
'storage-service-pending-deletes': ReadonlyArray<ExtendedStorageID>;
'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<GroupCredentialType>;
callLinkAuthCredentials: ReadonlyArray<GroupCredentialType>;
backupCombinedCredentials: ReadonlyArray<BackupCredentialWrapperType>;
backupCombinedCredentialsLastRequestTime: number;
backupMediaRootKey: Uint8Array;
backupMediaDownloadTotalBytes: number;
backupMediaDownloadCompletedBytes: number;
backupMediaDownloadPaused: boolean;
backupMediaDownloadBannerDismissed: boolean;
attachmentDownloadManagerIdled: boolean;
messageInsertTriggersDisabled: boolean;
setBackupMessagesSignatureKey: boolean;
setBackupMediaSignatureKey: boolean;
lastReceivedAtCounter: number;
preferredReactionEmoji: ReadonlyArray<string>;
emojiSkinToneDefault: EmojiSkinTone;
unreadCount: number;
'challenge:conversations': ReadonlyArray<RegisteredChallengeType>;
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<keyof StorageAccessType>;
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<keyof StorageAccessType>;
// Ensure every storage key is explicitly marked to be preserved or removed on unlink.
type AssertTrue<T extends true> = 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<StorageKeysToPreserveAfterUnlink, StorageKeysToRemoveAfterUnlink>,
never
>
>;
export type AssertStorageUnlinkKeysAreExhaustive = AssertTrue<
AssertSameMembers<
StorageKeysToPreserveAfterUnlink | StorageKeysToRemoveAfterUnlink,
keyof StorageAccessType
>
>;

View File

@@ -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<typeof themeSettingSchema>;
// Configuration keys that only affect UI
export const STORAGE_UI_KEYS: ReadonlyArray<keyof StorageAccessType> = [
'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',
];

View File

@@ -15,3 +15,12 @@ export type LocaleEmojiType = z.infer<typeof LocaleEmojiSchema>;
export const LocaleEmojiListSchema = LocaleEmojiSchema.array();
export type LocaleEmojiListType = z.infer<typeof LocaleEmojiListSchema>;
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
}

View File

@@ -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',

View File

@@ -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';

View File

@@ -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<typeof themeSettingSchema>;
export function themeClassName(theme: Theme): string {
switch (theme) {
case Theme.Light: