diff --git a/keyserver/src/scripts/generate-converter-from-validator.js b/keyserver/src/scripts/generate-converter-from-validator.js index 09ecdab53..0c2c75e61 100644 --- a/keyserver/src/scripts/generate-converter-from-validator.js +++ b/keyserver/src/scripts/generate-converter-from-validator.js @@ -1,234 +1,234 @@ // @flow import t, { type TInterface, type TType } from 'tcomb'; import { nativeMediaSelectionValidator, mediaValidator, } from 'lib/types/media-types.js'; import { threadPermissionInfoValidator } from 'lib/types/thread-permission-types.js'; import { rawThreadInfoValidator } from 'lib/types/thread-types.js'; -import { keyserverPrefixID, tID } from 'lib/utils/validation-utils.js'; +import { ashoatKeyserverID, tID } from 'lib/utils/validation-utils.js'; import { main } from './utils.js'; function getDiscriminatorFieldForUnionValidator(validator: TType) { if (validator === threadPermissionInfoValidator) { return 'value'; } if (validator === nativeMediaSelectionValidator) { return 'step'; } return 'type'; } function flattenInnerUnionValidators( innerValidators: $ReadOnlyArray>, ): TInterface<{ +[string]: mixed }>[] { let result = []; for (const innerValidator of innerValidators) { if (innerValidator.meta.kind === 'interface') { // In flow, union refinement only works if every variant has a key // that is a literal. In this case we don't get a refinement of // `innerValidator` because we are checking value of the inner // `meta` object. const recastValidator: TInterface<{ +[string]: mixed }> = (innerValidator: any); result.push(recastValidator); } else if (innerValidator.meta.kind === 'union') { result = [ ...result, ...flattenInnerUnionValidators(innerValidator.meta.types), ]; } else if ([t.String, t.Number, t.Boolean].includes(innerValidator)) { // We don't need to handle literal types because they can't be // converted } else { throw new Error( `Validator not supported in union: ${innerValidator.displayName}`, ); } } return result; } // MediaValidator is special cased because of flow issues function getConverterForMediaValidator(inputName: string) { return `(${inputName}.type === 'photo' ? { ...${inputName}, id: '256|' + ${inputName}.id } : ${inputName}.type === 'video' ? { ...${inputName}, id: '256|' + ${inputName}.id, thumbnailID: '256|' + ${inputName}.thumbnailID, } : ${inputName}.type === 'encrypted_photo' ? ({ ...${inputName}, id: '256|' + ${inputName}.id }: any) : ${inputName}.type === 'encrypted_video' ? ({ ...${inputName}, id: '256|' + ${inputName}.id, thumbnailID: '256|' + ${inputName}.thumbnailID, }: any) : ${inputName})`; } // `null` is returned if there is no conversion needed in T or any // of it's inner types function generateConverterFromValidator( validator: TType, inputName: string, validatorToBeConverted: TType, conversionExpressionString: (inputName: string) => string, ): ?string { if (validator === validatorToBeConverted) { return `(${conversionExpressionString(inputName)})`; } else if (validator === mediaValidator) { return getConverterForMediaValidator(inputName); } if (validator.meta.kind === 'maybe') { const inner = generateConverterFromValidator( validator.meta.type, inputName, validatorToBeConverted, conversionExpressionString, ); if (!inner) { return null; } return `((${inputName} !== null && ${inputName} !== undefined) ? (${inner}) : (${inputName}))`; } if (validator.meta.kind === 'subtype') { return generateConverterFromValidator( validator.meta.type, inputName, validatorToBeConverted, conversionExpressionString, ); } if (validator.meta.kind === 'interface') { // In flow, union refinement only works if every variant has a key // that is a literal. In this case we don't get a refinement of `validator` // because we are checking value of the inner `meta` object. const recastValidator: TInterface<{ +[string]: mixed }> = (validator: any); const fieldConverters = []; for (const key in recastValidator.meta.props) { const inner = generateConverterFromValidator( recastValidator.meta.props[key], `${inputName}.${key}`, validatorToBeConverted, conversionExpressionString, ); if (inner) { fieldConverters.push(`${key}:${inner}`); } } if (fieldConverters.length === 0) { return null; } return `({...${inputName}, ${fieldConverters.join(',')}})`; } if (validator.meta.kind === 'union') { const innerValidators = flattenInnerUnionValidators(validator.meta.types); const variantConverters = []; for (const innerValidator of innerValidators) { const discriminatorField = getDiscriminatorFieldForUnionValidator(validator); const discriminatorValidator = innerValidator.meta.props[discriminatorField]; if (!discriminatorValidator) { throw new Error( 'Union should have a discriminator ' + validator.displayName, ); } const discriminatorValue = discriminatorValidator.meta.name; const inner = generateConverterFromValidator( innerValidator, inputName, validatorToBeConverted, conversionExpressionString, ); if (inner) { variantConverters.push( `(${inputName}.${discriminatorField} === ${discriminatorValue}) ? (${inner})`, ); } } if (variantConverters.length === 0) { return null; } variantConverters.push(`(${inputName})`); return `(${variantConverters.join(':')})`; } if (validator.meta.kind === 'list') { const inner = generateConverterFromValidator( validator.meta.type, 'elem', validatorToBeConverted, conversionExpressionString, ); if (!inner) { return inputName; } return `(${inputName}.map(elem => ${inner}))`; } if (validator.meta.kind === 'dict') { const domainValidator = validator.meta.domain; const codomainValidator = validator.meta.codomain; let domainConverter = null; if (domainValidator === validatorToBeConverted) { domainConverter = conversionExpressionString('key'); } let codomainConverter = generateConverterFromValidator( codomainValidator, 'value', validatorToBeConverted, conversionExpressionString, ); if (!domainConverter && !codomainConverter) { return null; } domainConverter = domainConverter ?? 'key'; codomainConverter = codomainConverter ?? 'value'; return `(Object.fromEntries( entries(${inputName}).map( ([key, value]) => [${domainConverter}, ${codomainConverter}] ) ))`; } return null; } // Input arguments: const validator = rawThreadInfoValidator; const typeName = 'RawThreadInfo'; const validatorToBeConverted = tID; const conversionExpressionString = inputName => - `'${keyserverPrefixID}|' + ${inputName}`; + `'${ashoatKeyserverID}|' + ${inputName}`; main([ async () => { console.log( `export function convert${typeName}ToNewIDSchema(input: ${typeName}): ${typeName} { return`, generateConverterFromValidator( validator, 'input', validatorToBeConverted, conversionExpressionString, ) ?? 'input', ';}', ); }, ]); diff --git a/keyserver/src/utils/validation-utils.js b/keyserver/src/utils/validation-utils.js index 3e5899f01..b29d1d048 100644 --- a/keyserver/src/utils/validation-utils.js +++ b/keyserver/src/utils/validation-utils.js @@ -1,235 +1,235 @@ // @flow import type { TType } from 'tcomb'; import type { PolicyType } from 'lib/facts/policies.js'; import { hasMinCodeVersion, hasMinStateVersion, } from 'lib/shared/version-utils.js'; import { type PlatformDetails } from 'lib/types/device-types.js'; import { ServerError } from 'lib/utils/errors.js'; import { tCookie, tPassword, tPlatform, tPlatformDetails, assertWithValidator, - keyserverPrefixID, + ashoatKeyserverID, convertClientIDsToServerIDs, convertObject, convertServerIDsToClientIDs, } from 'lib/utils/validation-utils.js'; import { fetchNotAcknowledgedPolicies } from '../fetchers/policy-acknowledgment-fetchers.js'; import { verifyClientSupported } from '../session/version.js'; import type { Viewer } from '../session/viewer.js'; async function validateInput( viewer: Viewer, inputValidator: TType, input: mixed, ): Promise { if (!viewer.isSocket) { await checkClientSupported(viewer, inputValidator, input); } const convertedInput = checkInputValidator(inputValidator, input); if ( hasMinStateVersion(viewer.platformDetails, { native: 43, web: 3, }) ) { try { return convertClientIDsToServerIDs( - keyserverPrefixID, + ashoatKeyserverID, inputValidator, convertedInput, ); } catch (err) { throw new ServerError(err.message); } } return convertedInput; } function validateOutput( platformDetails: ?PlatformDetails, outputValidator: TType, data: T, alwaysConvertSchema?: boolean, ): T { if (!outputValidator.is(data)) { console.trace( 'Output validation failed, validator is:', outputValidator.displayName, ); return data; } if ( hasMinStateVersion(platformDetails, { native: 43, web: 3, }) || alwaysConvertSchema ) { return convertServerIDsToClientIDs( - keyserverPrefixID, + ashoatKeyserverID, outputValidator, data, ); } return data; } function checkInputValidator(inputValidator: TType, input: mixed): T { if (inputValidator.is(input)) { return assertWithValidator(input, inputValidator); } const error = new ServerError('invalid_parameters'); error.sanitizedInput = input ? sanitizeInput(inputValidator, input) : null; throw error; } async function checkClientSupported( viewer: Viewer, inputValidator: ?TType, input: T, ) { let platformDetails; if (inputValidator) { platformDetails = findFirstInputMatchingValidator( inputValidator, tPlatformDetails, input, ); } if (!platformDetails && inputValidator) { const platform = findFirstInputMatchingValidator( inputValidator, tPlatform, input, ); if (platform) { platformDetails = { platform }; } } if (!platformDetails) { ({ platformDetails } = viewer); } await verifyClientSupported(viewer, platformDetails); } const redactedString = '********'; const redactedTypes = [tPassword, tCookie]; function sanitizeInput(inputValidator: TType, input: T): T { return convertObject( inputValidator, input, redactedTypes, () => redactedString, ); } function findFirstInputMatchingValidator( wholeInputValidator: *, inputValidatorToMatch: *, input: *, ): any { if (!wholeInputValidator || input === null || input === undefined) { return null; } if ( wholeInputValidator === inputValidatorToMatch && wholeInputValidator.is(input) ) { return input; } if (wholeInputValidator.meta.kind === 'maybe') { return findFirstInputMatchingValidator( wholeInputValidator.meta.type, inputValidatorToMatch, input, ); } if ( wholeInputValidator.meta.kind === 'interface' && typeof input === 'object' ) { for (const key in input) { const value = input[key]; const validator = wholeInputValidator.meta.props[key]; const innerResult = findFirstInputMatchingValidator( validator, inputValidatorToMatch, value, ); if (innerResult) { return innerResult; } } } if (wholeInputValidator.meta.kind === 'union') { for (const validator of wholeInputValidator.meta.types) { if (validator.is(input)) { return findFirstInputMatchingValidator( validator, inputValidatorToMatch, input, ); } } } if (wholeInputValidator.meta.kind === 'list' && Array.isArray(input)) { const validator = wholeInputValidator.meta.type; for (const value of input) { const innerResult = findFirstInputMatchingValidator( validator, inputValidatorToMatch, value, ); if (innerResult) { return innerResult; } } } return null; } async function policiesValidator( viewer: Viewer, policies: $ReadOnlyArray, ) { if (!policies.length) { return; } if (!hasMinCodeVersion(viewer.platformDetails, { native: 181 })) { return; } const notAcknowledgedPolicies = await fetchNotAcknowledgedPolicies( viewer.id, policies, ); if (notAcknowledgedPolicies.length) { throw new ServerError('policies_not_accepted', { notAcknowledgedPolicies, }); } } export { validateInput, validateOutput, checkInputValidator, redactedString, sanitizeInput, findFirstInputMatchingValidator, checkClientSupported, policiesValidator, }; diff --git a/lib/selectors/socket-selectors.js b/lib/selectors/socket-selectors.js index 04ed6afe4..10bfb6070 100644 --- a/lib/selectors/socket-selectors.js +++ b/lib/selectors/socket-selectors.js @@ -1,252 +1,252 @@ // @flow import { createSelector } from 'reselect'; import t from 'tcomb'; import { currentCalendarQuery } from './nav-selectors.js'; import { serverEntryInfo, serverEntryInfosObject, filterRawEntryInfosByCalendarQuery, } from '../shared/entry-utils.js'; import threadWatcher from '../shared/thread-watcher.js'; import type { SignedIdentityKeysBlob } from '../types/crypto-types.js'; import { type RawEntryInfo, rawEntryInfoValidator, type CalendarQuery, } from '../types/entry-types.js'; import type { AppState } from '../types/redux-types.js'; import type { ClientReportCreationRequest } from '../types/report-types.js'; import { serverRequestTypes, type ClientServerRequest, type ClientClientResponse, } from '../types/request-types.js'; import type { SessionState } from '../types/session-types.js'; import type { OneTimeKeyGenerator } from '../types/socket-types.js'; import { type RawThreadInfo, rawThreadInfoValidator, } from '../types/thread-types.js'; import { type CurrentUserInfo, currentUserInfoValidator, type UserInfos, userInfosValidator, } from '../types/user-types.js'; import { getConfig } from '../utils/config.js'; import { minimumOneTimeKeysRequired } from '../utils/crypto-utils.js'; import { values, hash } from '../utils/objects.js'; import { tID, convertClientIDsToServerIDs, - keyserverPrefixID, + ashoatKeyserverID, } from '../utils/validation-utils.js'; const queuedReports: ( state: AppState, ) => $ReadOnlyArray = createSelector( (state: AppState) => state.reportStore.queuedReports, ( mainQueuedReports: $ReadOnlyArray, ): $ReadOnlyArray => mainQueuedReports, ); const getClientResponsesSelector: ( state: AppState, ) => ( calendarActive: boolean, oneTimeKeyGenerator: ?OneTimeKeyGenerator, getSignedIdentityKeysBlob: ?() => Promise, getInitialNotificationsEncryptedMessage: ?() => Promise, serverRequests: $ReadOnlyArray, ) => Promise<$ReadOnlyArray> = createSelector( (state: AppState) => state.threadStore.threadInfos, (state: AppState) => state.entryStore.entryInfos, (state: AppState) => state.userStore.userInfos, (state: AppState) => state.currentUserInfo, currentCalendarQuery, ( threadInfos: { +[id: string]: RawThreadInfo }, entryInfos: { +[id: string]: RawEntryInfo }, userInfos: UserInfos, currentUserInfo: ?CurrentUserInfo, calendarQuery: (calendarActive: boolean) => CalendarQuery, ) => { threadInfos = convertClientIDsToServerIDs( - keyserverPrefixID, + ashoatKeyserverID, t.dict(tID, rawThreadInfoValidator), threadInfos, ); userInfos = convertClientIDsToServerIDs( - keyserverPrefixID, + ashoatKeyserverID, userInfosValidator, userInfos, ); currentUserInfo = convertClientIDsToServerIDs( - keyserverPrefixID, + ashoatKeyserverID, t.maybe(currentUserInfoValidator), currentUserInfo, ); return async ( calendarActive: boolean, oneTimeKeyGenerator: ?OneTimeKeyGenerator, getSignedIdentityKeysBlob: ?() => Promise, getInitialNotificationsEncryptedMessage: ?() => Promise, serverRequests: $ReadOnlyArray, ): Promise<$ReadOnlyArray> => { const clientResponses = []; const serverRequestedPlatformDetails = serverRequests.some( request => request.type === serverRequestTypes.PLATFORM_DETAILS, ); for (const serverRequest of serverRequests) { if ( serverRequest.type === serverRequestTypes.PLATFORM && !serverRequestedPlatformDetails ) { clientResponses.push({ type: serverRequestTypes.PLATFORM, platform: getConfig().platformDetails.platform, }); } else if (serverRequest.type === serverRequestTypes.PLATFORM_DETAILS) { clientResponses.push({ type: serverRequestTypes.PLATFORM_DETAILS, platformDetails: getConfig().platformDetails, }); } else if (serverRequest.type === serverRequestTypes.CHECK_STATE) { let filteredEntryInfos = filterRawEntryInfosByCalendarQuery( serverEntryInfosObject(values(entryInfos)), calendarQuery(calendarActive), ); filteredEntryInfos = convertClientIDsToServerIDs( - keyserverPrefixID, + ashoatKeyserverID, t.dict(tID, rawEntryInfoValidator), filteredEntryInfos, ); const hashResults = {}; for (const key in serverRequest.hashesToCheck) { const expectedHashValue = serverRequest.hashesToCheck[key]; let hashValue; if (key === 'threadInfos') { hashValue = hash(threadInfos); } else if (key === 'entryInfos') { hashValue = hash(filteredEntryInfos); } else if (key === 'userInfos') { hashValue = hash(userInfos); } else if (key === 'currentUserInfo') { hashValue = hash(currentUserInfo); } else if (key.startsWith('threadInfo|')) { const [, threadID] = key.split('|'); hashValue = hash(threadInfos[threadID]); } else if (key.startsWith('entryInfo|')) { const [, entryID] = key.split('|'); let rawEntryInfo = filteredEntryInfos[entryID]; if (rawEntryInfo) { rawEntryInfo = serverEntryInfo(rawEntryInfo); } hashValue = hash(rawEntryInfo); } else if (key.startsWith('userInfo|')) { const [, userID] = key.split('|'); hashValue = hash(userInfos[userID]); } else { continue; } hashResults[key] = expectedHashValue === hashValue; } const { failUnmentioned } = serverRequest; if (failUnmentioned && failUnmentioned.threadInfos) { for (const threadID in threadInfos) { const key = `threadInfo|${threadID}`; const hashResult = hashResults[key]; if (hashResult === null || hashResult === undefined) { hashResults[key] = false; } } } if (failUnmentioned && failUnmentioned.entryInfos) { for (const entryID in filteredEntryInfos) { const key = `entryInfo|${entryID}`; const hashResult = hashResults[key]; if (hashResult === null || hashResult === undefined) { hashResults[key] = false; } } } if (failUnmentioned && failUnmentioned.userInfos) { for (const userID in userInfos) { const key = `userInfo|${userID}`; const hashResult = hashResults[key]; if (hashResult === null || hashResult === undefined) { hashResults[key] = false; } } } clientResponses.push({ type: serverRequestTypes.CHECK_STATE, hashResults, }); } else if ( serverRequest.type === serverRequestTypes.MORE_ONE_TIME_KEYS && oneTimeKeyGenerator ) { const keys: string[] = []; for (let i = 0; i < minimumOneTimeKeysRequired; ++i) { keys.push(oneTimeKeyGenerator(i)); } clientResponses.push({ type: serverRequestTypes.MORE_ONE_TIME_KEYS, keys, }); } else if ( serverRequest.type === serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB && getSignedIdentityKeysBlob ) { const signedIdentityKeysBlob = await getSignedIdentityKeysBlob(); clientResponses.push({ type: serverRequestTypes.SIGNED_IDENTITY_KEYS_BLOB, signedIdentityKeysBlob, }); } else if ( serverRequest.type === serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE && getInitialNotificationsEncryptedMessage ) { const initialNotificationsEncryptedMessage = await getInitialNotificationsEncryptedMessage(); clientResponses.push({ type: serverRequestTypes.INITIAL_NOTIFICATIONS_ENCRYPTED_MESSAGE, initialNotificationsEncryptedMessage, }); } } return clientResponses; }; }, ); const sessionStateFuncSelector: ( state: AppState, ) => (calendarActive: boolean) => SessionState = createSelector( (state: AppState) => state.messageStore.currentAsOf, (state: AppState) => state.updatesCurrentAsOf, currentCalendarQuery, ( messagesCurrentAsOf: number, updatesCurrentAsOf: number, calendarQuery: (calendarActive: boolean) => CalendarQuery, ) => (calendarActive: boolean): SessionState => ({ calendarQuery: calendarQuery(calendarActive), messagesCurrentAsOf, updatesCurrentAsOf, watchedIDs: threadWatcher.getWatchedIDs(), }), ); export { queuedReports, getClientResponsesSelector, sessionStateFuncSelector }; diff --git a/lib/shared/avatar-utils.js b/lib/shared/avatar-utils.js index aab8f5b8a..406130120 100644 --- a/lib/shared/avatar-utils.js +++ b/lib/shared/avatar-utils.js @@ -1,363 +1,363 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import stringHash from 'string-hash'; import { selectedThreadColors } from './color-utils.js'; import { threadOtherMembers } from './thread-utils.js'; import genesis from '../facts/genesis.js'; import { useENSAvatar } from '../hooks/ens-cache.js'; import { threadInfoSelector } from '../selectors/thread-selectors.js'; import { getETHAddressForUserInfo } from '../shared/account-utils.js'; import type { ClientEmojiAvatar, ClientAvatar, ResolvedClientAvatar, GenericUserInfoWithAvatar, } from '../types/avatar-types.js'; import { threadTypes } from '../types/thread-types-enum.js'; import { type RawThreadInfo, type ThreadInfo } from '../types/thread-types.js'; import type { UserInfos } from '../types/user-types.js'; import { useSelector } from '../utils/redux-utils.js'; -import { keyserverPrefixID } from '../utils/validation-utils.js'; +import { ashoatKeyserverID } from '../utils/validation-utils.js'; const defaultAnonymousUserEmojiAvatar: ClientEmojiAvatar = { color: selectedThreadColors[4], emoji: 'ðŸ‘Ī', type: 'emoji', }; const defaultEmojiAvatars: $ReadOnlyArray = [ { color: selectedThreadColors[0], emoji: '😀', type: 'emoji' }, { color: selectedThreadColors[1], emoji: '😃', type: 'emoji' }, { color: selectedThreadColors[3], emoji: '😄', type: 'emoji' }, { color: selectedThreadColors[3], emoji: '😁', type: 'emoji' }, { color: selectedThreadColors[4], emoji: '😆', type: 'emoji' }, { color: selectedThreadColors[6], emoji: '🙂', type: 'emoji' }, { color: selectedThreadColors[6], emoji: '😉', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '😊', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '😇', type: 'emoji' }, { color: selectedThreadColors[9], emoji: 'ðŸĨ°', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '😍', type: 'emoji' }, { color: selectedThreadColors[1], emoji: 'ðŸĪĐ', type: 'emoji' }, { color: selectedThreadColors[3], emoji: 'ðŸĨģ', type: 'emoji' }, { color: selectedThreadColors[9], emoji: '😝', type: 'emoji' }, { color: selectedThreadColors[3], emoji: '😎', type: 'emoji' }, { color: selectedThreadColors[4], emoji: '🧐', type: 'emoji' }, { color: selectedThreadColors[7], emoji: 'ðŸĨļ', type: 'emoji' }, { color: selectedThreadColors[3], emoji: 'ðŸĪ—', type: 'emoji' }, { color: selectedThreadColors[0], emoji: 'ðŸ˜Ī', type: 'emoji' }, { color: selectedThreadColors[0], emoji: 'ðŸĪŊ', type: 'emoji' }, { color: selectedThreadColors[1], emoji: 'ðŸĪ”', type: 'emoji' }, { color: selectedThreadColors[8], emoji: 'ðŸŦĄ', type: 'emoji' }, { color: selectedThreadColors[9], emoji: 'ðŸĪŦ', type: 'emoji' }, { color: selectedThreadColors[3], emoji: 'ðŸ˜Ū', type: 'emoji' }, { color: selectedThreadColors[4], emoji: 'ðŸ˜ē', type: 'emoji' }, { color: selectedThreadColors[5], emoji: 'ðŸĪ ', type: 'emoji' }, { color: selectedThreadColors[6], emoji: 'ðŸĪ‘', type: 'emoji' }, { color: selectedThreadColors[7], emoji: 'ðŸ‘Đ‍🚀', type: 'emoji' }, { color: selectedThreadColors[2], emoji: 'ðŸĨ·', type: 'emoji' }, { color: selectedThreadColors[5], emoji: 'ðŸ‘ŧ', type: 'emoji' }, { color: selectedThreadColors[1], emoji: 'ðŸ‘ū', type: 'emoji' }, { color: selectedThreadColors[2], emoji: 'ðŸĪ–', type: 'emoji' }, { color: selectedThreadColors[3], emoji: '😚', type: 'emoji' }, { color: selectedThreadColors[4], emoji: 'ðŸ˜ļ', type: 'emoji' }, { color: selectedThreadColors[9], emoji: 'ðŸ˜đ', type: 'emoji' }, { color: selectedThreadColors[6], emoji: 'ðŸ˜ŧ', type: 'emoji' }, { color: selectedThreadColors[1], emoji: 'ðŸŽĐ', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '👑', type: 'emoji' }, { color: selectedThreadColors[9], emoji: 'ðŸķ', type: 'emoji' }, { color: selectedThreadColors[0], emoji: 'ðŸą', type: 'emoji' }, { color: selectedThreadColors[2], emoji: '🐭', type: 'emoji' }, { color: selectedThreadColors[3], emoji: 'ðŸđ', type: 'emoji' }, { color: selectedThreadColors[4], emoji: '🐰', type: 'emoji' }, { color: selectedThreadColors[5], emoji: 'ðŸŧ', type: 'emoji' }, { color: selectedThreadColors[6], emoji: '🐞', type: 'emoji' }, { color: selectedThreadColors[7], emoji: 'ðŸŧ‍❄ïļ', type: 'emoji' }, { color: selectedThreadColors[6], emoji: 'ðŸĻ', type: 'emoji' }, { color: selectedThreadColors[9], emoji: 'ðŸŊ', type: 'emoji' }, { color: selectedThreadColors[1], emoji: 'ðŸĶ', type: 'emoji' }, { color: selectedThreadColors[2], emoji: 'ðŸļ', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '🐔', type: 'emoji' }, { color: selectedThreadColors[4], emoji: '🐧', type: 'emoji' }, { color: selectedThreadColors[5], emoji: 'ðŸĶ', type: 'emoji' }, { color: selectedThreadColors[6], emoji: 'ðŸĪ', type: 'emoji' }, { color: selectedThreadColors[7], emoji: 'ðŸĶ„', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '🐝', type: 'emoji' }, { color: selectedThreadColors[9], emoji: 'ðŸĶ‹', type: 'emoji' }, { color: selectedThreadColors[5], emoji: '🐎', type: 'emoji' }, { color: selectedThreadColors[1], emoji: 'ðŸģ', type: 'emoji' }, { color: selectedThreadColors[2], emoji: '🐋', type: 'emoji' }, { color: selectedThreadColors[3], emoji: 'ðŸĶˆ', type: 'emoji' }, { color: selectedThreadColors[4], emoji: 'ðŸĶ­', type: 'emoji' }, { color: selectedThreadColors[5], emoji: '🐘', type: 'emoji' }, { color: selectedThreadColors[6], emoji: 'ðŸĶ›', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '🐐', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '🐓', type: 'emoji' }, { color: selectedThreadColors[9], emoji: 'ðŸĶƒ', type: 'emoji' }, { color: selectedThreadColors[0], emoji: 'ðŸĶĐ', type: 'emoji' }, { color: selectedThreadColors[1], emoji: 'ðŸĶ”', type: 'emoji' }, { color: selectedThreadColors[2], emoji: '🐅', type: 'emoji' }, { color: selectedThreadColors[1], emoji: '🐆', type: 'emoji' }, { color: selectedThreadColors[5], emoji: 'ðŸĶ“', type: 'emoji' }, { color: selectedThreadColors[4], emoji: 'ðŸĶ’', type: 'emoji' }, { color: selectedThreadColors[6], emoji: 'ðŸĶ˜', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '🐎', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '🐕', type: 'emoji' }, { color: selectedThreadColors[1], emoji: 'ðŸĐ', type: 'emoji' }, { color: selectedThreadColors[0], emoji: 'ðŸĶŪ', type: 'emoji' }, { color: selectedThreadColors[1], emoji: '🐈', type: 'emoji' }, { color: selectedThreadColors[2], emoji: 'ðŸĶš', type: 'emoji' }, { color: selectedThreadColors[4], emoji: 'ðŸĶœ', type: 'emoji' }, { color: selectedThreadColors[4], emoji: 'ðŸĶĒ', type: 'emoji' }, { color: selectedThreadColors[5], emoji: '🕊ïļ', type: 'emoji' }, { color: selectedThreadColors[6], emoji: '🐇', type: 'emoji' }, { color: selectedThreadColors[7], emoji: 'ðŸĶĶ', type: 'emoji' }, { color: selectedThreadColors[8], emoji: 'ðŸŋïļ', type: 'emoji' }, { color: selectedThreadColors[9], emoji: '🐉', type: 'emoji' }, { color: selectedThreadColors[0], emoji: 'ðŸŒī', type: 'emoji' }, { color: selectedThreadColors[7], emoji: 'ðŸŒą', type: 'emoji' }, { color: selectedThreadColors[2], emoji: '☘ïļ', type: 'emoji' }, { color: selectedThreadColors[3], emoji: '🍀', type: 'emoji' }, { color: selectedThreadColors[4], emoji: '🍄', type: 'emoji' }, { color: selectedThreadColors[5], emoji: 'ðŸŒŋ', type: 'emoji' }, { color: selectedThreadColors[8], emoji: 'ðŸŠī', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '🍁', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '💐', type: 'emoji' }, { color: selectedThreadColors[9], emoji: '🌷', type: 'emoji' }, { color: selectedThreadColors[0], emoji: 'ðŸŒđ', type: 'emoji' }, { color: selectedThreadColors[1], emoji: 'ðŸŒļ', type: 'emoji' }, { color: selectedThreadColors[7], emoji: 'ðŸŒŧ', type: 'emoji' }, { color: selectedThreadColors[3], emoji: '⭐', type: 'emoji' }, { color: selectedThreadColors[4], emoji: '🌟', type: 'emoji' }, { color: selectedThreadColors[5], emoji: '🍏', type: 'emoji' }, { color: selectedThreadColors[6], emoji: '🍎', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '🍐', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '🍊', type: 'emoji' }, { color: selectedThreadColors[9], emoji: '🍋', type: 'emoji' }, { color: selectedThreadColors[0], emoji: '🍓', type: 'emoji' }, { color: selectedThreadColors[1], emoji: 'ðŸŦ', type: 'emoji' }, { color: selectedThreadColors[3], emoji: '🍈', type: 'emoji' }, { color: selectedThreadColors[2], emoji: '🍒', type: 'emoji' }, { color: selectedThreadColors[4], emoji: 'ðŸĨ­', type: 'emoji' }, { color: selectedThreadColors[9], emoji: '🍍', type: 'emoji' }, { color: selectedThreadColors[1], emoji: 'ðŸĨ', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '🍅', type: 'emoji' }, { color: selectedThreadColors[8], emoji: 'ðŸĨĶ', type: 'emoji' }, { color: selectedThreadColors[9], emoji: 'ðŸĨ•', type: 'emoji' }, { color: selectedThreadColors[0], emoji: 'ðŸĨ', type: 'emoji' }, { color: selectedThreadColors[1], emoji: 'ðŸĨŊ', type: 'emoji' }, { color: selectedThreadColors[6], emoji: '🍞', type: 'emoji' }, { color: selectedThreadColors[3], emoji: 'ðŸĨ–', type: 'emoji' }, { color: selectedThreadColors[4], emoji: 'ðŸĨĻ', type: 'emoji' }, { color: selectedThreadColors[6], emoji: '🧀', type: 'emoji' }, { color: selectedThreadColors[6], emoji: 'ðŸĨž', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '🧇', type: 'emoji' }, { color: selectedThreadColors[8], emoji: 'ðŸĨ“', type: 'emoji' }, { color: selectedThreadColors[9], emoji: '🍔', type: 'emoji' }, { color: selectedThreadColors[0], emoji: '🍟', type: 'emoji' }, { color: selectedThreadColors[1], emoji: '🍕', type: 'emoji' }, { color: selectedThreadColors[2], emoji: 'ðŸĨ—', type: 'emoji' }, { color: selectedThreadColors[3], emoji: '🍝', type: 'emoji' }, { color: selectedThreadColors[4], emoji: '🍜', type: 'emoji' }, { color: selectedThreadColors[5], emoji: 'ðŸē', type: 'emoji' }, { color: selectedThreadColors[6], emoji: '🍛', type: 'emoji' }, { color: selectedThreadColors[7], emoji: 'ðŸĢ', type: 'emoji' }, { color: selectedThreadColors[8], emoji: 'ðŸą', type: 'emoji' }, { color: selectedThreadColors[9], emoji: 'ðŸĨŸ', type: 'emoji' }, { color: selectedThreadColors[0], emoji: 'ðŸĪ', type: 'emoji' }, { color: selectedThreadColors[1], emoji: '🍙', type: 'emoji' }, { color: selectedThreadColors[2], emoji: '🍚', type: 'emoji' }, { color: selectedThreadColors[3], emoji: 'ðŸĨ', type: 'emoji' }, { color: selectedThreadColors[4], emoji: 'ðŸĶ', type: 'emoji' }, { color: selectedThreadColors[6], emoji: '🧁', type: 'emoji' }, { color: selectedThreadColors[5], emoji: '🍭', type: 'emoji' }, { color: selectedThreadColors[7], emoji: 'ðŸĐ', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '🍊', type: 'emoji' }, { color: selectedThreadColors[9], emoji: '☕ïļ', type: 'emoji' }, { color: selectedThreadColors[0], emoji: 'ðŸĩ', type: 'emoji' }, { color: selectedThreadColors[1], emoji: 'âš―ïļ', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '🏀', type: 'emoji' }, { color: selectedThreadColors[9], emoji: '🏈', type: 'emoji' }, { color: selectedThreadColors[4], emoji: 'âšūïļ', type: 'emoji' }, { color: selectedThreadColors[0], emoji: 'ðŸĨŽ', type: 'emoji' }, { color: selectedThreadColors[8], emoji: 'ðŸŽū', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '🏐', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '🏉', type: 'emoji' }, { color: selectedThreadColors[3], emoji: 'ðŸŽą', type: 'emoji' }, { color: selectedThreadColors[0], emoji: '🏆', type: 'emoji' }, { color: selectedThreadColors[1], emoji: 'ðŸŽĻ', type: 'emoji' }, { color: selectedThreadColors[2], emoji: 'ðŸŽĪ', type: 'emoji' }, { color: selectedThreadColors[3], emoji: '🎧', type: 'emoji' }, { color: selectedThreadColors[4], emoji: '🎞', type: 'emoji' }, { color: selectedThreadColors[5], emoji: 'ðŸŽđ', type: 'emoji' }, { color: selectedThreadColors[6], emoji: 'ðŸĨ', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '🎷', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '🎚', type: 'emoji' }, { color: selectedThreadColors[9], emoji: 'ðŸŽļ', type: 'emoji' }, { color: selectedThreadColors[0], emoji: '🊕', type: 'emoji' }, { color: selectedThreadColors[1], emoji: 'ðŸŽŧ', type: 'emoji' }, { color: selectedThreadColors[2], emoji: 'ðŸŽē', type: 'emoji' }, { color: selectedThreadColors[3], emoji: '♟ïļ', type: 'emoji' }, { color: selectedThreadColors[4], emoji: 'ðŸŽŪ', type: 'emoji' }, { color: selectedThreadColors[5], emoji: '🚗', type: 'emoji' }, { color: selectedThreadColors[3], emoji: '🚙', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '🚌', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '🏎ïļ', type: 'emoji' }, { color: selectedThreadColors[6], emoji: 'ðŸ›ŧ', type: 'emoji' }, { color: selectedThreadColors[0], emoji: '🚚', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '🚛', type: 'emoji' }, { color: selectedThreadColors[2], emoji: '🚘', type: 'emoji' }, { color: selectedThreadColors[0], emoji: '🚀', type: 'emoji' }, { color: selectedThreadColors[4], emoji: '🚁', type: 'emoji' }, { color: selectedThreadColors[5], emoji: 'ðŸ›ķ', type: 'emoji' }, { color: selectedThreadColors[6], emoji: 'â›ĩïļ', type: 'emoji' }, { color: selectedThreadColors[7], emoji: 'ðŸšĪ', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '⚓', type: 'emoji' }, { color: selectedThreadColors[6], emoji: '🏰', type: 'emoji' }, { color: selectedThreadColors[0], emoji: 'ðŸŽĄ', type: 'emoji' }, { color: selectedThreadColors[1], emoji: '💎', type: 'emoji' }, { color: selectedThreadColors[2], emoji: 'ðŸ”Ū', type: 'emoji' }, { color: selectedThreadColors[9], emoji: '💈', type: 'emoji' }, { color: selectedThreadColors[4], emoji: 'ðŸ§ļ', type: 'emoji' }, { color: selectedThreadColors[9], emoji: '🎊', type: 'emoji' }, { color: selectedThreadColors[4], emoji: '🎉', type: 'emoji' }, { color: selectedThreadColors[7], emoji: 'ðŸŠĐ', type: 'emoji' }, { color: selectedThreadColors[8], emoji: '🚂', type: 'emoji' }, { color: selectedThreadColors[3], emoji: '🚆', type: 'emoji' }, { color: selectedThreadColors[1], emoji: '🚊', type: 'emoji' }, { color: selectedThreadColors[1], emoji: '🛰ïļ', type: 'emoji' }, { color: selectedThreadColors[7], emoji: '🏠', type: 'emoji' }, { color: selectedThreadColors[3], emoji: '⛰ïļ', type: 'emoji' }, { color: selectedThreadColors[4], emoji: '🏔ïļ', type: 'emoji' }, { color: selectedThreadColors[5], emoji: 'ðŸ—ŧ', type: 'emoji' }, { color: selectedThreadColors[6], emoji: '🏛ïļ', type: 'emoji' }, { color: selectedThreadColors[7], emoji: 'â›Đïļ', type: 'emoji' }, { color: selectedThreadColors[8], emoji: 'ðŸ§ē', type: 'emoji' }, { color: selectedThreadColors[9], emoji: '🎁', type: 'emoji' }, ]; function getRandomDefaultEmojiAvatar(): ClientEmojiAvatar { const randomIndex = Math.floor(Math.random() * defaultEmojiAvatars.length); return defaultEmojiAvatars[randomIndex]; } function getDefaultAvatar(hashKey: string, color?: string): ClientEmojiAvatar { let key = hashKey; - if (key.startsWith(`${keyserverPrefixID}|`)) { - key = key.slice(`${keyserverPrefixID}|`.length); + if (key.startsWith(`${ashoatKeyserverID}|`)) { + key = key.slice(`${ashoatKeyserverID}|`.length); } const avatarIndex = stringHash(key) % defaultEmojiAvatars.length; return { ...defaultEmojiAvatars[avatarIndex], color: color ? color : defaultEmojiAvatars[avatarIndex].color, }; } function getAvatarForUser(user: ?GenericUserInfoWithAvatar): ClientAvatar { if (user?.avatar) { return user.avatar; } if (!user?.username) { return defaultAnonymousUserEmojiAvatar; } return getDefaultAvatar(user.username); } function getUserAvatarForThread( threadInfo: RawThreadInfo | ThreadInfo, viewerID: ?string, userInfos: UserInfos, ): ClientAvatar { if (threadInfo.type === threadTypes.PRIVATE) { invariant(viewerID, 'viewerID should be set for PRIVATE threads'); return getAvatarForUser(userInfos[viewerID]); } invariant( threadInfo.type === threadTypes.PERSONAL, 'threadInfo should be a PERSONAL type', ); const memberInfos = threadOtherMembers(threadInfo.members, viewerID) .map(member => userInfos[member.id] && userInfos[member.id]) .filter(Boolean); if (memberInfos.length === 0) { return defaultAnonymousUserEmojiAvatar; } return getAvatarForUser(memberInfos[0]); } function getAvatarForThread( thread: RawThreadInfo | ThreadInfo, containingThreadInfo: ?ThreadInfo, ): ClientAvatar { if (thread.avatar) { return thread.avatar; } if (containingThreadInfo && containingThreadInfo.id !== genesis.id) { return containingThreadInfo.avatar ? containingThreadInfo.avatar : getDefaultAvatar(containingThreadInfo.id, containingThreadInfo.color); } return getDefaultAvatar(thread.id, thread.color); } function useAvatarForThread(thread: RawThreadInfo | ThreadInfo): ClientAvatar { const containingThreadID = thread.containingThreadID; const containingThreadInfo = useSelector(state => containingThreadID ? threadInfoSelector(state)[containingThreadID] : null, ); return getAvatarForThread(thread, containingThreadInfo); } function useENSResolvedAvatar( avatarInfo: ClientAvatar, userInfo: ?GenericUserInfoWithAvatar, ): ResolvedClientAvatar { const ethAddress = React.useMemo( () => getETHAddressForUserInfo(userInfo), [userInfo], ); const ensAvatarURI = useENSAvatar(ethAddress); const resolvedAvatar = React.useMemo(() => { if (avatarInfo.type !== 'ens') { return avatarInfo; } if (ensAvatarURI) { return { type: 'image', uri: ensAvatarURI, }; } return defaultAnonymousUserEmojiAvatar; }, [ensAvatarURI, avatarInfo]); return resolvedAvatar; } export { defaultAnonymousUserEmojiAvatar, defaultEmojiAvatars, getRandomDefaultEmojiAvatar, getDefaultAvatar, getAvatarForUser, getUserAvatarForThread, getAvatarForThread, useAvatarForThread, useENSResolvedAvatar, }; diff --git a/lib/utils/migration-utils.js b/lib/utils/migration-utils.js index 850b40827..01129bacb 100644 --- a/lib/utils/migration-utils.js +++ b/lib/utils/migration-utils.js @@ -1,122 +1,122 @@ // @flow import type { TranslatedThreadMessageInfos } from './message-ops-utils.js'; import { entries } from './objects.js'; -import { keyserverPrefixID } from './validation-utils.js'; +import { ashoatKeyserverID } from './validation-utils.js'; import { convertRawMessageInfoToNewIDSchema, convertRawThreadInfoToNewIDSchema, } from '../_generated/migration-utils.js'; import { parsePendingThreadID, getPendingThreadID, draftKeySuffix, } from '../shared/thread-utils.js'; import type { ClientDBDraftInfo, ClientDBDraftStoreOperation, DraftStore, } from '../types/draft-types'; import type { RawMessageInfo } from '../types/message-types.js'; import type { ThreadStoreThreadInfos } from '../types/thread-types.js'; function convertDraftKeyToNewIDSchema(key: string): string { const threadID = key.slice(0, -draftKeySuffix.length); const pendingIDContents = parsePendingThreadID(threadID); if (!pendingIDContents) { - return `${keyserverPrefixID}|${key}`; + return `${ashoatKeyserverID}|${key}`; } const { threadType, sourceMessageID, memberIDs } = pendingIDContents; if (!sourceMessageID) { return key; } const convertedThreadID = getPendingThreadID( threadType, memberIDs, - `${keyserverPrefixID}|${sourceMessageID}`, + `${ashoatKeyserverID}|${sourceMessageID}`, ); return `${convertedThreadID}${draftKeySuffix}`; } function convertDraftStoreToNewIDSchema(store: DraftStore): DraftStore { return { drafts: Object.fromEntries( entries(store.drafts).map(([key, value]) => [ convertDraftKeyToNewIDSchema(key), value, ]), ), }; } function generateIDSchemaMigrationOpsForDrafts( drafts: $ReadOnlyArray, ): $ReadOnlyArray { const operations = drafts.map(draft => ({ type: 'update', payload: { key: convertDraftKeyToNewIDSchema(draft.key), text: draft.text, }, })); return [{ type: 'remove_all' }, ...operations]; } function convertMessageStoreThreadsToNewIDSchema( messageStoreThreads: TranslatedThreadMessageInfos, ): TranslatedThreadMessageInfos { return Object.fromEntries( entries(messageStoreThreads).map(([id, translatedThreadMessageInfo]) => [ - `${keyserverPrefixID}|` + id, + `${ashoatKeyserverID}|` + id, translatedThreadMessageInfo, ]), ); } function convertThreadStoreThreadInfosToNewIDSchema( threadStoreThreadInfos: ThreadStoreThreadInfos, ): ThreadStoreThreadInfos { return Object.fromEntries( entries(threadStoreThreadInfos).map(([id, threadInfo]) => [ - `${keyserverPrefixID}|` + id, + `${ashoatKeyserverID}|` + id, convertRawThreadInfoToNewIDSchema(threadInfo), ]), ); } function convertNotificationThreadIDToNewIDSchema(threadID: string): string { if (threadID.indexOf('|') === -1) { - return `${keyserverPrefixID}|${threadID}`; + return `${ashoatKeyserverID}|${threadID}`; } return threadID; } function convertNotificationMessageInfoToNewIDSchema( messageInfosString: ?string, ): ?$ReadOnlyArray { let messageInfos: ?$ReadOnlyArray = null; if (messageInfosString) { messageInfos = JSON.parse(messageInfosString); } if (messageInfos?.some(message => message.threadID.indexOf('|') === -1)) { messageInfos = messageInfos?.map(convertRawMessageInfoToNewIDSchema); } return messageInfos; } export { convertDraftKeyToNewIDSchema, convertDraftStoreToNewIDSchema, generateIDSchemaMigrationOpsForDrafts, convertMessageStoreThreadsToNewIDSchema, convertThreadStoreThreadInfosToNewIDSchema, convertNotificationThreadIDToNewIDSchema, convertNotificationMessageInfoToNewIDSchema, }; diff --git a/lib/utils/validation-utils.js b/lib/utils/validation-utils.js index efaa98341..8da2bf684 100644 --- a/lib/utils/validation-utils.js +++ b/lib/utils/validation-utils.js @@ -1,248 +1,248 @@ // @flow import invariant from 'invariant'; import _mapKeys from 'lodash/fp/mapKeys.js'; import _mapValues from 'lodash/fp/mapValues.js'; import t from 'tcomb'; import type { TStructProps, TIrreducible, TRefinement, TEnums, TInterface, TUnion, TType, } from 'tcomb'; import { validEmailRegex, oldValidUsernameRegex, validHexColorRegex, } from '../shared/account-utils.js'; import type { PlatformDetails } from '../types/device-types'; import type { MediaMessageServerDBContent, PhotoMessageServerDBContent, VideoMessageServerDBContent, } from '../types/messages/media'; function tBool(value: boolean): TIrreducible { return t.irreducible(value.toString(), x => x === value); } function tString(value: string): TIrreducible { return t.irreducible(`'${value}'`, x => x === value); } function tNumber(value: number): TIrreducible { return t.irreducible(value.toString(), x => x === value); } function tShape(spec: TStructProps): TInterface { return t.interface(spec, { strict: true }); } type TRegex = TRefinement; function tRegex(regex: RegExp): TRegex { return t.refinement(t.String, val => regex.test(val)); } function tNumEnum(nums: $ReadOnlyArray): TRefinement { return t.refinement(t.Number, (input: number) => { for (const num of nums) { if (input === num) { return true; } } return false; }); } const tNull: TIrreducible = t.irreducible('null', x => x === null); const tDate: TRegex = tRegex(/^[0-9]{4}-[0-1][0-9]-[0-3][0-9]$/); const tColor: TRegex = tRegex(validHexColorRegex); // we don't include # char const tPlatform: TEnums = t.enums.of([ 'ios', 'android', 'web', 'windows', 'macos', ]); const tDeviceType: TEnums = t.enums.of(['ios', 'android']); const tPlatformDetails: TInterface = tShape({ platform: tPlatform, codeVersion: t.maybe(t.Number), stateVersion: t.maybe(t.Number), }); const tPassword: TRefinement = t.refinement( t.String, (password: string) => !!password, ); const tCookie: TRegex = tRegex(/^(user|anonymous)=[0-9]+:[0-9a-f]+$/); const tEmail: TRegex = tRegex(validEmailRegex); const tOldValidUsername: TRegex = tRegex(oldValidUsernameRegex); const tID: TRefinement = t.refinement(t.String, (id: string) => !!id); const tMediaMessagePhoto: TInterface = tShape({ type: tString('photo'), uploadID: tID, }); const tMediaMessageVideo: TInterface = tShape({ type: tString('video'), uploadID: tID, thumbnailUploadID: tID, }); const tMediaMessageMedia: TUnion = t.union([ tMediaMessagePhoto, tMediaMessageVideo, ]); function assertWithValidator(data: mixed, validator: TType): T { invariant(validator.is(data), "data isn't of type T"); return (data: any); } -const keyserverPrefixID = '256'; +const ashoatKeyserverID = '256'; function convertServerIDsToClientIDs( serverPrefixID: string, outputValidator: TType, data: T, ): T { const conversionFunction = id => { if (id.indexOf('|') !== -1) { console.warn(`Server id '${id}' already has a prefix`); return id; } return `${serverPrefixID}|${id}`; }; return convertObject(outputValidator, data, [tID], conversionFunction); } function convertClientIDsToServerIDs( serverPrefixID: string, outputValidator: TType, data: T, ): T { const prefix = serverPrefixID + '|'; const conversionFunction = id => { if (id.startsWith(prefix)) { return id.substr(prefix.length); } throw new Error('invalid_client_id_prefix'); }; return convertObject(outputValidator, data, [tID], conversionFunction); } function convertObject( validator: TType, input: I, typesToConvert: $ReadOnlyArray>, conversionFunction: T => T, ): I { if (input === null || input === undefined) { return input; } // While they should be the same runtime object, // `TValidator` is `TType` and `validator` is `TType`. // Having them have different types allows us to use `assertWithValidator` // to change `input` flow type const TValidator = typesToConvert[typesToConvert.indexOf(validator)]; if (TValidator && TValidator.is(input)) { const TInput = assertWithValidator(input, TValidator); const converted = conversionFunction(TInput); return assertWithValidator(converted, validator); } if (validator.meta.kind === 'maybe' || validator.meta.kind === 'subtype') { return convertObject( validator.meta.type, input, typesToConvert, conversionFunction, ); } if (validator.meta.kind === 'interface' && typeof input === 'object') { const recastValidator: TInterface = (validator: any); const result = {}; for (const key in input) { const innerValidator = recastValidator.meta.props[key]; result[key] = convertObject( innerValidator, input[key], typesToConvert, conversionFunction, ); } return assertWithValidator(result, recastValidator); } if (validator.meta.kind === 'union') { for (const innerValidator of validator.meta.types) { if (innerValidator.is(input)) { return convertObject( innerValidator, input, typesToConvert, conversionFunction, ); } } return input; } if (validator.meta.kind === 'list' && Array.isArray(input)) { const innerValidator = validator.meta.type; return (input.map(value => convertObject(innerValidator, value, typesToConvert, conversionFunction), ): any); } if (validator.meta.kind === 'dict' && typeof input === 'object') { const domainValidator = validator.meta.domain; const codomainValidator = validator.meta.codomain; if (typesToConvert.includes(domainValidator)) { input = _mapKeys(key => conversionFunction(key))(input); } return _mapValues(value => convertObject( codomainValidator, value, typesToConvert, conversionFunction, ), )(input); } return input; } export { tBool, tString, tNumber, tShape, tRegex, tNumEnum, tNull, tDate, tColor, tPlatform, tDeviceType, tPlatformDetails, tPassword, tCookie, tEmail, tOldValidUsername, tID, tMediaMessagePhoto, tMediaMessageVideo, tMediaMessageMedia, assertWithValidator, - keyserverPrefixID, + ashoatKeyserverID, convertClientIDsToServerIDs, convertServerIDsToClientIDs, convertObject, }; diff --git a/native/redux/persist.js b/native/redux/persist.js index 17da2b72a..7e61f7113 100644 --- a/native/redux/persist.js +++ b/native/redux/persist.js @@ -1,765 +1,765 @@ // @flow import AsyncStorage from '@react-native-async-storage/async-storage'; import invariant from 'invariant'; import { Platform } from 'react-native'; import Orientation from 'react-native-orientation-locker'; import { createTransform } from 'redux-persist'; import type { Transform } from 'redux-persist/es/types.js'; import { convertEntryStoreToNewIDSchema, convertInviteLinksStoreToNewIDSchema, convertMessageStoreToNewIDSchema, convertRawMessageInfoToNewIDSchema, convertCalendarFilterToNewIDSchema, convertConnectionInfoToNewIDSchema, } from 'lib/_generated/migration-utils.js'; import { type ReportStoreOperation, type ClientDBReportStoreOperation, convertReportStoreOperationToClientDBReportStoreOperation, convertReportsToReplaceReportOps, } from 'lib/ops/report-store-ops.js'; import { highestLocalIDSelector } from 'lib/selectors/local-id-selectors.js'; import { createAsyncMigrate } from 'lib/shared/create-async-migrate.js'; import { inconsistencyResponsesToReports } from 'lib/shared/report-utils.js'; import { getContainingThreadID, getCommunity, } from 'lib/shared/thread-utils.js'; import { DEPRECATED_unshimMessageStore, unshimFunc, } from 'lib/shared/unshim-utils.js'; import { defaultEnabledApps } from 'lib/types/enabled-apps.js'; import { defaultCalendarFilters } from 'lib/types/filter-types.js'; import { messageTypes } from 'lib/types/message-types-enum.js'; import { type LocalMessageInfo, type MessageStore, type ClientDBMessageStoreOperation, } from 'lib/types/message-types.js'; import type { ReportStore, ClientReportCreationRequest, } from 'lib/types/report-types.js'; import { defaultConnectionInfo } from 'lib/types/socket-types.js'; import type { ClientDBThreadStoreOperation, ClientDBThreadInfo, } from 'lib/types/thread-types.js'; import { convertMessageStoreOperationsToClientDBOperations, translateClientDBMessageInfoToRawMessageInfo, translateRawMessageInfoToClientDBMessageInfo, } from 'lib/utils/message-ops-utils.js'; import { generateIDSchemaMigrationOpsForDrafts, convertMessageStoreThreadsToNewIDSchema, convertThreadStoreThreadInfosToNewIDSchema, } from 'lib/utils/migration-utils.js'; import { defaultNotifPermissionAlertInfo } from 'lib/utils/push-alerts.js'; import { convertClientDBThreadInfoToRawThreadInfo, convertRawThreadInfoToClientDBThreadInfo, convertThreadStoreOperationsToClientDBOperations, } from 'lib/utils/thread-ops-utils.js'; import { getUUID } from 'lib/utils/uuid.js'; -import { keyserverPrefixID } from 'lib/utils/validation-utils.js'; +import { ashoatKeyserverID } from 'lib/utils/validation-utils.js'; import { updateClientDBThreadStoreThreadInfos, createUpdateDBOpsForThreadStoreThreadInfos, createUpdateDBOpsForMessageStoreMessages, createUpdateDBOpsForMessageStoreThreads, } from './client-db-utils.js'; import { migrateThreadStoreForEditThreadPermissions } from './edit-thread-permission-migration.js'; import { persistMigrationForManagePinsThreadPermission } from './manage-pins-permission-migration.js'; import type { AppState } from './state-types.js'; import { unshimClientDB } from './unshim-utils.js'; import { updateRolesAndPermissions } from './update-roles-and-permissions.js'; import { commCoreModule } from '../native-modules.js'; import { defaultDeviceCameraInfo } from '../types/camera.js'; import { defaultGlobalThemeInfo } from '../types/themes.js'; import { isTaskCancelledError } from '../utils/error-handling.js'; const migrations = { [1]: (state: AppState) => ({ ...state, notifPermissionAlertInfo: defaultNotifPermissionAlertInfo, }), [2]: (state: AppState) => ({ ...state, messageSentFromRoute: [], }), [3]: state => ({ currentUserInfo: state.currentUserInfo, entryStore: state.entryStore, threadInfos: state.threadInfos, userInfos: state.userInfos, messageStore: { ...state.messageStore, currentAsOf: state.currentAsOf, }, updatesCurrentAsOf: state.currentAsOf, cookie: state.cookie, deviceToken: state.deviceToken, urlPrefix: state.urlPrefix, customServer: state.customServer, notifPermissionAlertInfo: state.notifPermissionAlertInfo, messageSentFromRoute: state.messageSentFromRoute, _persist: state._persist, }), [4]: (state: AppState) => ({ ...state, pingTimestamps: undefined, activeServerRequests: undefined, }), [5]: (state: AppState) => ({ ...state, calendarFilters: defaultCalendarFilters, }), [6]: state => ({ ...state, threadInfos: undefined, threadStore: { threadInfos: state.threadInfos, inconsistencyResponses: [], }, }), [7]: state => ({ ...state, lastUserInteraction: undefined, sessionID: undefined, entryStore: { ...state.entryStore, inconsistencyResponses: [], }, }), [8]: (state: AppState) => ({ ...state, pingTimestamps: undefined, activeServerRequests: undefined, connection: defaultConnectionInfo(Platform.OS), watchedThreadIDs: [], entryStore: { ...state.entryStore, actualizedCalendarQuery: undefined, }, }), [9]: (state: AppState) => ({ ...state, connection: { ...state.connection, lateResponses: [], }, }), [10]: (state: AppState) => ({ ...state, nextLocalID: highestLocalIDSelector(state) + 1, connection: { ...state.connection, showDisconnectedBar: false, }, messageStore: { ...state.messageStore, local: {}, }, }), [11]: (state: AppState) => ({ ...state, messageStore: DEPRECATED_unshimMessageStore(state.messageStore, [ messageTypes.IMAGES, ]), }), [12]: (state: AppState) => ({ ...state, globalThemeInfo: defaultGlobalThemeInfo, }), [13]: (state: AppState) => ({ ...state, deviceCameraInfo: defaultDeviceCameraInfo, deviceOrientation: Orientation.getInitialOrientation(), }), [14]: (state: AppState) => state, [15]: state => ({ ...state, threadStore: { ...state.threadStore, inconsistencyReports: inconsistencyResponsesToReports( state.threadStore.inconsistencyResponses, ), inconsistencyResponses: undefined, }, entryStore: { ...state.entryStore, inconsistencyReports: inconsistencyResponsesToReports( state.entryStore.inconsistencyResponses, ), inconsistencyResponses: undefined, }, queuedReports: [], }), [16]: state => { const result = { ...state, messageSentFromRoute: undefined, dataLoaded: !!state.currentUserInfo && !state.currentUserInfo.anonymous, }; if (state.navInfo) { result.navInfo = { ...state.navInfo, navigationState: undefined, }; } return result; }, [17]: state => ({ ...state, userInfos: undefined, userStore: { userInfos: state.userInfos, inconsistencyResponses: [], }, }), [18]: state => ({ ...state, userStore: { userInfos: state.userStore.userInfos, inconsistencyReports: [], }, }), [19]: state => { const threadInfos = {}; for (const threadID in state.threadStore.threadInfos) { const threadInfo = state.threadStore.threadInfos[threadID]; const { visibilityRules, ...rest } = threadInfo; threadInfos[threadID] = rest; } return { ...state, threadStore: { ...state.threadStore, threadInfos, }, }; }, [20]: (state: AppState) => ({ ...state, messageStore: DEPRECATED_unshimMessageStore(state.messageStore, [ messageTypes.UPDATE_RELATIONSHIP, ]), }), [21]: (state: AppState) => ({ ...state, messageStore: DEPRECATED_unshimMessageStore(state.messageStore, [ messageTypes.CREATE_SIDEBAR, messageTypes.SIDEBAR_SOURCE, ]), }), [22]: state => { for (const key in state.drafts) { const value = state.drafts[key]; try { commCoreModule.updateDraft(key, value); } catch (e) { if (!isTaskCancelledError(e)) { throw e; } } } return { ...state, drafts: undefined, }; }, [23]: state => ({ ...state, globalThemeInfo: defaultGlobalThemeInfo, }), [24]: state => ({ ...state, enabledApps: defaultEnabledApps, }), [25]: state => ({ ...state, crashReportsEnabled: __DEV__, }), [26]: state => { const { currentUserInfo } = state; if (currentUserInfo.anonymous) { return state; } return { ...state, crashReportsEnabled: undefined, currentUserInfo: { id: currentUserInfo.id, username: currentUserInfo.username, }, enabledReports: { crashReports: __DEV__, inconsistencyReports: __DEV__, mediaReports: __DEV__, }, }; }, [27]: state => ({ ...state, queuedReports: undefined, enabledReports: undefined, threadStore: { ...state.threadStore, inconsistencyReports: undefined, }, entryStore: { ...state.entryStore, inconsistencyReports: undefined, }, reportStore: { enabledReports: { crashReports: __DEV__, inconsistencyReports: __DEV__, mediaReports: __DEV__, }, queuedReports: [ ...state.entryStore.inconsistencyReports, ...state.threadStore.inconsistencyReports, ...state.queuedReports, ], }, }), [28]: state => { const threadParentToChildren = {}; for (const threadID in state.threadStore.threadInfos) { const threadInfo = state.threadStore.threadInfos[threadID]; const parentThreadInfo = threadInfo.parentThreadID ? state.threadStore.threadInfos[threadInfo.parentThreadID] : null; const parentIndex = parentThreadInfo ? parentThreadInfo.id : '-1'; if (!threadParentToChildren[parentIndex]) { threadParentToChildren[parentIndex] = []; } threadParentToChildren[parentIndex].push(threadID); } const rootIDs = threadParentToChildren['-1']; if (!rootIDs) { // This should never happen, but if it somehow does we'll let the state // check mechanism resolve it... return state; } const threadInfos = {}; const stack = [...rootIDs]; while (stack.length > 0) { const threadID = stack.shift(); const threadInfo = state.threadStore.threadInfos[threadID]; const parentThreadInfo = threadInfo.parentThreadID ? threadInfos[threadInfo.parentThreadID] : null; threadInfos[threadID] = { ...threadInfo, containingThreadID: getContainingThreadID( parentThreadInfo, threadInfo.type, ), community: getCommunity(parentThreadInfo), }; const children = threadParentToChildren[threadID]; if (children) { stack.push(...children); } } return { ...state, threadStore: { ...state.threadStore, threadInfos } }; }, [29]: (state: AppState) => { const updatedThreadInfos = migrateThreadStoreForEditThreadPermissions( state.threadStore.threadInfos, ); return { ...state, threadStore: { ...state.threadStore, threadInfos: updatedThreadInfos, }, }; }, [30]: (state: AppState) => { const threadInfos = state.threadStore.threadInfos; const operations = [ { type: 'remove_all', }, ...Object.keys(threadInfos).map((id: string) => ({ type: 'replace', payload: { id, threadInfo: threadInfos[id] }, })), ]; try { commCoreModule.processThreadStoreOperationsSync( convertThreadStoreOperationsToClientDBOperations(operations), ); } catch (exception) { console.log(exception); if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [31]: (state: AppState) => { const messages = state.messageStore.messages; const operations: $ReadOnlyArray = [ { type: 'remove_all', }, ...Object.keys(messages).map((id: string) => ({ type: 'replace', payload: translateRawMessageInfoToClientDBMessageInfo(messages[id]), })), ]; try { commCoreModule.processMessageStoreOperationsSync(operations); } catch (exception) { console.log(exception); if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [32]: (state: AppState) => unshimClientDB(state, [messageTypes.MULTIMEDIA]), [33]: (state: AppState) => unshimClientDB(state, [messageTypes.REACTION]), [34]: state => { const { threadIDsToNotifIDs, ...stateSansThreadIDsToNotifIDs } = state; return stateSansThreadIDsToNotifIDs; }, [35]: (state: AppState) => unshimClientDB(state, [messageTypes.MULTIMEDIA]), [36]: (state: AppState) => { // 1. Get threads and messages from SQLite `threads` and `messages` tables. const clientDBThreadInfos = commCoreModule.getAllThreadsSync(); const clientDBMessageInfos = commCoreModule.getAllMessagesSync(); // 2. Translate `ClientDBThreadInfo`s to `RawThreadInfo`s and // `ClientDBMessageInfo`s to `RawMessageInfo`s. const rawThreadInfos = clientDBThreadInfos.map( convertClientDBThreadInfoToRawThreadInfo, ); const rawMessageInfos = clientDBMessageInfos.map( translateClientDBMessageInfoToRawMessageInfo, ); // 3. Unshim translated `RawMessageInfos` to get the TOGGLE_PIN messages const unshimmedRawMessageInfos = rawMessageInfos.map(messageInfo => unshimFunc(messageInfo, new Set([messageTypes.TOGGLE_PIN])), ); // 4. Filter out non-TOGGLE_PIN messages const filteredRawMessageInfos = unshimmedRawMessageInfos.filter( messageInfo => messageInfo.type === messageTypes.TOGGLE_PIN, ); // 5. We want only the last TOGGLE_PIN message for each message ID, // so 'pin', 'unpin', 'pin' don't count as 3 pins, but only 1. const lastMessageIDToRawMessageInfoMap = new Map(); for (const messageInfo of filteredRawMessageInfos) { const { targetMessageID } = messageInfo; lastMessageIDToRawMessageInfoMap.set(targetMessageID, messageInfo); } const lastMessageIDToRawMessageInfos = Array.from( lastMessageIDToRawMessageInfoMap.values(), ); // 6. Create a Map of threadIDs to pinnedCount const threadIDsToPinnedCount = new Map(); for (const messageInfo of lastMessageIDToRawMessageInfos) { const { threadID, type } = messageInfo; if (type === messageTypes.TOGGLE_PIN) { const pinnedCount = threadIDsToPinnedCount.get(threadID) || 0; threadIDsToPinnedCount.set(threadID, pinnedCount + 1); } } // 7. Include a pinnedCount for each rawThreadInfo const rawThreadInfosWithPinnedCount = rawThreadInfos.map(threadInfo => ({ ...threadInfo, pinnedCount: threadIDsToPinnedCount.get(threadInfo.id) || 0, })); // 8. Convert rawThreadInfos to a map of threadID to threadInfo const threadIDToThreadInfo = rawThreadInfosWithPinnedCount.reduce( (acc, threadInfo) => { acc[threadInfo.id] = threadInfo; return acc; }, {}, ); // 9. Add threadPermission to each threadInfo const rawThreadInfosWithThreadPermission = persistMigrationForManagePinsThreadPermission(threadIDToThreadInfo); // 10. Convert the new threadInfos back into an array const rawThreadInfosWithCountAndPermission = Object.keys( rawThreadInfosWithThreadPermission, ).map(id => rawThreadInfosWithThreadPermission[id]); // 11. Translate `RawThreadInfo`s to `ClientDBThreadInfo`s. const convertedClientDBThreadInfos = rawThreadInfosWithCountAndPermission.map( convertRawThreadInfoToClientDBThreadInfo, ); // 12. Construct `ClientDBThreadStoreOperation`s to clear SQLite `threads` // table and repopulate with `ClientDBThreadInfo`s. const operations: $ReadOnlyArray = [ { type: 'remove_all', }, ...convertedClientDBThreadInfos.map((thread: ClientDBThreadInfo) => ({ type: 'replace', payload: thread, })), ]; // 13. Try processing `ClientDBThreadStoreOperation`s and log out if // `processThreadStoreOperationsSync(...)` throws an exception. try { commCoreModule.processThreadStoreOperationsSync(operations); } catch (exception) { console.log(exception); return { ...state, cookie: null }; } return state; }, [37]: state => { const operations = convertMessageStoreOperationsToClientDBOperations([ { type: 'remove_all_threads', }, { type: 'replace_threads', payload: { threads: state.messageStore.threads }, }, ]); try { commCoreModule.processMessageStoreOperationsSync(operations); } catch (exception) { console.error(exception); if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [38]: state => updateClientDBThreadStoreThreadInfos(state, updateRolesAndPermissions), [39]: (state: AppState) => unshimClientDB(state, [messageTypes.EDIT_MESSAGE]), [40]: state => updateClientDBThreadStoreThreadInfos(state, updateRolesAndPermissions), [41]: (state: AppState) => { const queuedReports = state.reportStore.queuedReports.map(report => ({ ...report, id: getUUID(), })); return { ...state, reportStore: { ...state.reportStore, queuedReports }, }; }, [42]: (state: AppState) => { const reportStoreOperations: $ReadOnlyArray = [ { type: 'remove_all_reports' }, ...convertReportsToReplaceReportOps(state.reportStore.queuedReports), ]; const dbOperations: $ReadOnlyArray = convertReportStoreOperationToClientDBReportStoreOperation( reportStoreOperations, ); try { commCoreModule.processReportStoreOperationsSync(dbOperations); } catch (exception) { if (isTaskCancelledError(exception)) { return state; } return { ...state, cookie: null }; } return state; }, [43]: async (state: AppState) => { const { messages, drafts, threads, messageStoreThreads } = await commCoreModule.getClientDBStore(); const messageStoreThreadsOperations = createUpdateDBOpsForMessageStoreThreads( messageStoreThreads, convertMessageStoreThreadsToNewIDSchema, ); const messageStoreMessagesOperations = createUpdateDBOpsForMessageStoreMessages(messages, messageInfos => messageInfos.map(convertRawMessageInfoToNewIDSchema), ); const threadOperations = createUpdateDBOpsForThreadStoreThreadInfos( threads, convertThreadStoreThreadInfosToNewIDSchema, ); const draftOperations = generateIDSchemaMigrationOpsForDrafts(drafts); try { await Promise.all([ commCoreModule.processMessageStoreOperations([ ...messageStoreMessagesOperations, ...messageStoreThreadsOperations, ]), commCoreModule.processThreadStoreOperations(threadOperations), commCoreModule.processDraftStoreOperations(draftOperations), ]); } catch (exception) { console.log(exception); return { ...state, cookie: null }; } return { ...state, entryStore: convertEntryStoreToNewIDSchema(state.entryStore), messageStore: convertMessageStoreToNewIDSchema(state.messageStore), calendarFilters: state.calendarFilters.map( convertCalendarFilterToNewIDSchema, ), connection: convertConnectionInfoToNewIDSchema(state.connection), watchedThreadIDs: state.watchedThreadIDs.map( - id => `${keyserverPrefixID}|${id}`, + id => `${ashoatKeyserverID}|${id}`, ), inviteLinksStore: convertInviteLinksStoreToNewIDSchema( state.inviteLinksStore, ), }; }, }; // After migration 31, we'll no longer want to persist `messageStore.messages` // via redux-persist. However, we DO want to continue persisting everything in // `messageStore` EXCEPT for `messages`. The `blacklist` property in // `persistConfig` allows us to specify top-level keys that shouldn't be // persisted. However, we aren't able to specify nested keys in `blacklist`. // As a result, if we want to prevent nested keys from being persisted we'll // need to use `createTransform(...)` to specify an `inbound` function that // allows us to modify the `state` object before it's passed through // `JSON.stringify(...)` and written to disk. We specify the keys for which // this transformation should be executed in the `whitelist` property of the // `config` object that's passed to `createTransform(...)`. // eslint-disable-next-line no-unused-vars type PersistedThreadMessageInfo = { +startReached: boolean, +lastNavigatedTo: number, +lastPruned: number, }; type PersistedMessageStore = { +local: { +[id: string]: LocalMessageInfo }, +currentAsOf: number, +threads: { +[threadID: string]: PersistedThreadMessageInfo }, }; const messageStoreMessagesBlocklistTransform: Transform = createTransform( (state: MessageStore): PersistedMessageStore => { const { messages, threads, ...messageStoreSansMessages } = state; // We also do not want to persist `messageStore.threads[ID].messageIDs` // because they can be deterministically computed based on messages we have // from SQLite const threadsToPersist = {}; for (const threadID in threads) { const { messageIDs, ...threadsData } = threads[threadID]; threadsToPersist[threadID] = threadsData; } return { ...messageStoreSansMessages, threads: threadsToPersist }; }, (state: MessageStore): MessageStore => { const { threads: persistedThreads, ...messageStore } = state; const threads = {}; for (const threadID in persistedThreads) { threads[threadID] = { ...persistedThreads[threadID], messageIDs: [] }; } // We typically expect `messageStore.messages` to be `undefined` because // messages are persisted in the SQLite `messages` table rather than via // `redux-persist`. In this case we want to set `messageStore.messages` // to {} so we don't run into issues with `messageStore.messages` being // `undefined` (https://phab.comm.dev/D5545). // // However, in the case that a user is upgrading from a client where // `persistConfig.version` < 31, we expect `messageStore.messages` to // contain messages stored via `redux-persist` that we need in order // to correctly populate the SQLite `messages` table in migration 31 // (https://phab.comm.dev/D2600). // // However, because `messageStoreMessagesBlocklistTransform` modifies // `messageStore` before migrations are run, we need to make sure we aren't // inadvertently clearing `messageStore.messages` (by setting to {}) before // messages are stored in SQLite (https://linear.app/comm/issue/ENG-2377). return { ...messageStore, threads, messages: messageStore.messages ?? {} }; }, { whitelist: ['messageStore'] }, ); type PersistedReportStore = $Diff< ReportStore, { +queuedReports: $ReadOnlyArray }, >; const reportStoreTransform: Transform = createTransform( (state: ReportStore): PersistedReportStore => { return { enabledReports: state.enabledReports }; }, (state: PersistedReportStore): ReportStore => { return { ...state, queuedReports: [] }; }, { whitelist: ['reportStore'] }, ); const persistConfig = { key: 'root', storage: AsyncStorage, blacklist: [ 'loadingStatuses', 'lifecycleState', 'dimensions', 'draftStore', 'connectivity', 'deviceOrientation', 'frozen', 'threadStore', 'storeLoaded', ], debug: __DEV__, version: 43, transforms: [messageStoreMessagesBlocklistTransform, reportStoreTransform], migrate: (createAsyncMigrate(migrations, { debug: __DEV__ }): any), timeout: ((__DEV__ ? 0 : undefined): number | void), }; const codeVersion: number = commCoreModule.getCodeVersion(); // This local exists to avoid a circular dependency where redux-setup needs to // import all the navigation and screen stuff, but some of those screens want to // access the persistor to purge its state. let storedPersistor = null; function setPersistor(persistor: *) { storedPersistor = persistor; } function getPersistor(): empty { invariant(storedPersistor, 'should be set'); return storedPersistor; } export { persistConfig, codeVersion, setPersistor, getPersistor };