diff --git a/native/account/registration/registration-server-call.js b/native/account/registration/registration-server-call.js index 051840629..e03ceed27 100644 --- a/native/account/registration/registration-server-call.js +++ b/native/account/registration/registration-server-call.js @@ -1,610 +1,613 @@ // @flow import * as React from 'react'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { setSyncedMetadataEntryActionType } from 'lib/actions/synced-metadata-actions.js'; import { legacyKeyserverRegisterActionTypes, legacyKeyserverRegister, useIdentityPasswordRegister, identityRegisterActionTypes, deleteAccountActionTypes, useDeleteDiscardedIdentityAccount, } from 'lib/actions/user-actions.js'; import { useIsLoggedInToAuthoritativeKeyserver } from 'lib/hooks/account-hooks.js'; import { useKeyserverAuthWithRetry } from 'lib/keyserver-conn/keyserver-auth.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; import { usePreRequestUserState } from 'lib/selectors/account-selectors.js'; import { type LegacyLogInStartingPayload, logInActionSources, type LogOutResult, } from 'lib/types/account-types.js'; import { syncedMetadataNames } from 'lib/types/synced-metadata-types.js'; import { getMessageForException } from 'lib/utils/errors.js'; import { NO_FID_METADATA } from 'lib/utils/farcaster-utils.js'; import { useDispatchActionPromise } from 'lib/utils/redux-promise-utils.js'; import { useDispatch } from 'lib/utils/redux-utils.js'; import { usingCommServicesAccessToken } from 'lib/utils/services-utils.js'; import { setURLPrefix } from 'lib/utils/url-utils.js'; import { waitUntilDatabaseDeleted } from 'lib/utils/wait-until-db-deleted.js'; import type { RegistrationServerCallInput, UsernameAccountSelection, EthereumAccountSelection, AvatarData, } from './registration-types.js'; import { authoritativeKeyserverID } from '../../authoritative-keyserver.js'; import { useNativeSetUserAvatar, useUploadSelectedMedia, } from '../../avatars/avatar-hooks.js'; import { commCoreModule } from '../../native-modules.js'; import { persistConfig } from '../../redux/persist.js'; import { useSelector } from '../../redux/redux-utils.js'; import { nativeLegacyLogInExtraInfoSelector } from '../../selectors/account-selectors.js'; import { appOutOfDateAlertDetails, usernameReservedAlertDetails, usernameTakenAlertDetails, unknownErrorAlertDetails, } from '../../utils/alert-messages.js'; import Alert from '../../utils/alert.js'; import { defaultURLPrefix } from '../../utils/url-utils.js'; import { setNativeCredentials } from '../native-credentials.js'; import { useLegacySIWEServerCall, useIdentityWalletRegisterCall, } from '../siwe-hooks.js'; // We can't just do everything in one async callback, since the server calls // would get bound to Redux state from before the registration. The registration // flow has multiple steps where critical Redux state is changed, where // subsequent steps depend on accessing the updated Redux state. // To address this, we break the registration process up into multiple steps. // When each step completes we update the currentStep state, and we have Redux // selectors that trigger useEffects for subsequent steps when relevant data // starts to appear in Redux. type CurrentStep = | { +step: 'inactive' } | { +step: 'identity_registration_dispatched', +clearCachedSelections: () => void, +onAlertAcknowledged: ?() => mixed, +avatarData: ?AvatarData, +credentialsToSave: ?{ +username: string, +password: string }, +resolve: () => void, +reject: Error => void, } | { +step: 'authoritative_keyserver_registration_dispatched', +clearCachedSelections: () => void, +avatarData: ?AvatarData, +credentialsToSave: ?{ +username: string, +password: string }, +resolve: () => void, +reject: Error => void, }; const inactiveStep = { step: 'inactive' }; function useRegistrationServerCall(): RegistrationServerCallInput => Promise { const [currentStep, setCurrentStep] = React.useState(inactiveStep); // STEP 1: ACCOUNT REGISTRATION const legacyLogInExtraInfo = useSelector(nativeLegacyLogInExtraInfoSelector); const dispatchActionPromise = useDispatchActionPromise(); const callLegacyKeyserverRegister = useLegacyAshoatKeyserverCall( legacyKeyserverRegister, ); const callIdentityPasswordRegister = useIdentityPasswordRegister(); const identityRegisterUsernameAccount = React.useCallback( async ( accountSelection: UsernameAccountSelection, farcasterID: ?string, onAlertAcknowledged: ?() => mixed, ) => { const identityRegisterPromise = (async () => { try { return await callIdentityPasswordRegister( accountSelection.username, accountSelection.password, farcasterID, ); } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'username_reserved') { Alert.alert( usernameReservedAlertDetails.title, usernameReservedAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else if (messageForException === 'username_already_exists') { Alert.alert( usernameTakenAlertDetails.title, usernameTakenAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else if (messageForException === 'unsupported_version') { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } throw e; } })(); void dispatchActionPromise( identityRegisterActionTypes, identityRegisterPromise, ); await identityRegisterPromise; }, [callIdentityPasswordRegister, dispatchActionPromise], ); const legacyKeyserverRegisterUsernameAccount = React.useCallback( async ( accountSelection: UsernameAccountSelection, keyserverURL: string, onAlertAcknowledged: ?() => mixed, ) => { const extraInfo = await legacyLogInExtraInfo(); const legacyKeyserverRegisterPromise = (async () => { try { return await callLegacyKeyserverRegister( { ...extraInfo, username: accountSelection.username, password: accountSelection.password, }, { urlPrefixOverride: keyserverURL, }, ); } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'username_reserved') { Alert.alert( usernameReservedAlertDetails.title, usernameReservedAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else if (messageForException === 'username_taken') { Alert.alert( usernameTakenAlertDetails.title, usernameTakenAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else if (messageForException === 'client_version_unsupported') { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } throw e; } })(); void dispatchActionPromise( legacyKeyserverRegisterActionTypes, legacyKeyserverRegisterPromise, undefined, ({ calendarQuery: extraInfo.calendarQuery, }: LegacyLogInStartingPayload), ); await legacyKeyserverRegisterPromise; }, [legacyLogInExtraInfo, callLegacyKeyserverRegister, dispatchActionPromise], ); const legacySiweServerCall = useLegacySIWEServerCall(); const legacyKeyserverRegisterEthereumAccount = React.useCallback( async ( accountSelection: EthereumAccountSelection, keyserverURL: string, onAlertAcknowledged: ?() => mixed, ) => { try { await legacySiweServerCall(accountSelection, { urlPrefixOverride: keyserverURL, }); } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'client_version_unsupported') { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } throw e; } }, [legacySiweServerCall], ); const identityWalletRegisterCall = useIdentityWalletRegisterCall(); const identityRegisterEthereumAccount = React.useCallback( async ( accountSelection: EthereumAccountSelection, farcasterID: ?string, onNonceExpired: () => mixed, onAlertAcknowledged: ?() => mixed, ) => { try { await identityWalletRegisterCall({ address: accountSelection.address, message: accountSelection.message, signature: accountSelection.signature, fid: farcasterID, }); } catch (e) { const messageForException = getMessageForException(e); if (messageForException === 'nonce_expired') { onNonceExpired(); } else if (messageForException === 'unsupported_version') { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } throw e; } }, [identityWalletRegisterCall], ); const dispatch = useDispatch(); const returnedFunc = React.useCallback( (input: RegistrationServerCallInput) => new Promise( // eslint-disable-next-line no-async-promise-executor async (resolve, reject) => { try { if (currentStep.step !== 'inactive') { return; } const { accountSelection, avatarData, keyserverURL: passedKeyserverURL, farcasterID, siweBackupSecrets, clearCachedSelections, onNonceExpired, onAlertAcknowledged, } = input; const keyserverURL = passedKeyserverURL ?? defaultURLPrefix; if ( accountSelection.accountType === 'username' && !usingCommServicesAccessToken ) { await legacyKeyserverRegisterUsernameAccount( accountSelection, keyserverURL, onAlertAcknowledged, ); } else if (accountSelection.accountType === 'username') { await identityRegisterUsernameAccount( accountSelection, farcasterID, onAlertAcknowledged, ); } else if (!usingCommServicesAccessToken) { await legacyKeyserverRegisterEthereumAccount( accountSelection, keyserverURL, onAlertAcknowledged, ); } else { await identityRegisterEthereumAccount( accountSelection, farcasterID, onNonceExpired, onAlertAcknowledged, ); } if (passedKeyserverURL) { dispatch({ type: setURLPrefix, payload: passedKeyserverURL, }); } const fidToSave = farcasterID ?? NO_FID_METADATA; dispatch({ type: setSyncedMetadataEntryActionType, payload: { name: syncedMetadataNames.CURRENT_USER_FID, data: fidToSave, }, }); if (siweBackupSecrets) { await commCoreModule.setSIWEBackupSecrets(siweBackupSecrets); } const credentialsToSave = accountSelection.accountType === 'username' ? { username: accountSelection.username, password: accountSelection.password, } : null; if (usingCommServicesAccessToken) { setCurrentStep({ step: 'identity_registration_dispatched', avatarData, clearCachedSelections, onAlertAcknowledged, credentialsToSave, resolve, reject, }); } else { setCurrentStep({ step: 'authoritative_keyserver_registration_dispatched', avatarData, clearCachedSelections, credentialsToSave, resolve, reject, }); } } catch (e) { reject(e); } }, ), [ currentStep, legacyKeyserverRegisterUsernameAccount, identityRegisterUsernameAccount, legacyKeyserverRegisterEthereumAccount, identityRegisterEthereumAccount, dispatch, ], ); // STEP 2: REGISTERING ON AUTHORITATIVE KEYSERVER const keyserverAuth = useKeyserverAuthWithRetry(authoritativeKeyserverID); const isRegisteredOnIdentity = useSelector( state => !!state.commServicesAccessToken && !!state.currentUserInfo && !state.currentUserInfo.anonymous, ); // We call deleteDiscardedIdentityAccount in order to reset state if identity // registration succeeds but authoritative keyserver auth fails const deleteDiscardedIdentityAccount = useDeleteDiscardedIdentityAccount(); const preRequestUserState = usePreRequestUserState(); const commServicesAccessToken = useSelector( state => state.commServicesAccessToken, ); const registeringOnAuthoritativeKeyserverRef = React.useRef(false); React.useEffect(() => { if ( !isRegisteredOnIdentity || currentStep.step !== 'identity_registration_dispatched' || registeringOnAuthoritativeKeyserverRef.current ) { return; } registeringOnAuthoritativeKeyserverRef.current = true; const { avatarData, clearCachedSelections, onAlertAcknowledged, credentialsToSave, resolve, reject, } = currentStep; void (async () => { try { await keyserverAuth({ authActionSource: process.env.BROWSER ? logInActionSources.keyserverAuthFromWeb : logInActionSources.keyserverAuthFromNative, setInProgress: () => {}, hasBeenCancelled: () => false, doNotRegister: false, password: credentialsToSave?.password, }); setCurrentStep({ step: 'authoritative_keyserver_registration_dispatched', avatarData, clearCachedSelections, credentialsToSave, resolve, reject, }); } catch (keyserverAuthException) { const messageForException = getMessageForException( keyserverAuthException, ); const discardIdentityAccountPromise: Promise = (async () => { try { const deletionResult = await deleteDiscardedIdentityAccount( credentialsToSave?.password, ); if (messageForException === 'client_version_unsupported') { Alert.alert( appOutOfDateAlertDetails.title, appOutOfDateAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } else { Alert.alert( unknownErrorAlertDetails.title, unknownErrorAlertDetails.message, [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); } return deletionResult; } catch (deleteException) { // We swallow the exception here because // discardIdentityAccountPromise is used in a scenario where the // user is visibly logged-out, and it's only used to reset state // (eg. Redux, SQLite) to a logged-out state. The state reset // only occurs when a success action is dispatched, so by // swallowing exceptions we ensure that we always dispatch a // success. Alert.alert( 'Account created but login failed', 'We were able to create your account, but were unable to log ' + 'you in. Try going back to the login screen and logging in ' + 'with your new credentials.', [{ text: 'OK', onPress: onAlertAcknowledged }], { cancelable: !onAlertAcknowledged }, ); return { currentUserInfo: null, preRequestUserState: { ...preRequestUserState, commServicesAccessToken, }, }; } })(); void dispatchActionPromise( deleteAccountActionTypes, discardIdentityAccountPromise, ); await waitUntilDatabaseDeleted(); reject(keyserverAuthException); setCurrentStep(inactiveStep); } finally { registeringOnAuthoritativeKeyserverRef.current = false; } })(); }, [ currentStep, isRegisteredOnIdentity, keyserverAuth, dispatchActionPromise, deleteDiscardedIdentityAccount, preRequestUserState, commServicesAccessToken, ]); // STEP 3: SETTING AVATAR const uploadSelectedMedia = useUploadSelectedMedia(); const nativeSetUserAvatar = useNativeSetUserAvatar(); const isLoggedInToAuthKeyserver = useIsLoggedInToAuthoritativeKeyserver(); const avatarBeingSetRef = React.useRef(false); React.useEffect(() => { if ( !isLoggedInToAuthKeyserver || currentStep.step !== 'authoritative_keyserver_registration_dispatched' || avatarBeingSetRef.current ) { return; } avatarBeingSetRef.current = true; const { avatarData, resolve, clearCachedSelections, credentialsToSave } = currentStep; void (async () => { try { if (!avatarData) { return; } let updateUserAvatarRequest; if (!avatarData.needsUpload) { ({ updateUserAvatarRequest } = avatarData); } else { const { mediaSelection } = avatarData; - updateUserAvatarRequest = await uploadSelectedMedia(mediaSelection); + updateUserAvatarRequest = await uploadSelectedMedia( + mediaSelection, + 'keyserver', + ); if (!updateUserAvatarRequest) { return; } } await nativeSetUserAvatar(updateUserAvatarRequest); } finally { dispatch({ type: setSyncedMetadataEntryActionType, payload: { name: syncedMetadataNames.DB_VERSION, data: `${persistConfig.version}`, }, }); dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); clearCachedSelections(); if (credentialsToSave) { void setNativeCredentials(credentialsToSave); } setCurrentStep(inactiveStep); avatarBeingSetRef.current = false; resolve(); } })(); }, [ currentStep, isLoggedInToAuthKeyserver, uploadSelectedMedia, nativeSetUserAvatar, dispatch, ]); return returnedFunc; } export { useRegistrationServerCall }; diff --git a/native/avatars/avatar-hooks.js b/native/avatars/avatar-hooks.js index 94211f30d..3c75b898e 100644 --- a/native/avatars/avatar-hooks.js +++ b/native/avatars/avatar-hooks.js @@ -1,580 +1,604 @@ // @flow import { useActionSheet } from '@expo/react-native-action-sheet'; import * as ImagePicker from 'expo-image-picker'; import invariant from 'invariant'; import * as React from 'react'; import { Platform } from 'react-native'; import filesystem from 'react-native-fs'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { uploadMultimedia, useBlobServiceUpload, } from 'lib/actions/upload-actions.js'; import { EditThreadAvatarContext } from 'lib/components/base-edit-thread-avatar-provider.react.js'; import { EditUserAvatarContext } from 'lib/components/edit-user-avatar-provider.react.js'; import { useLegacyAshoatKeyserverCall } from 'lib/keyserver-conn/legacy-keyserver-call.js'; import { extensionFromFilename, filenameFromPathOrURI, } from 'lib/media/file-utils.js'; -import type { - AvatarDBContent, - UpdateUserAvatarRequest, -} from 'lib/types/avatar-types.js'; +import type { UpdateUserAvatarRequest } from 'lib/types/avatar-types.js'; import type { NativeMediaSelection, MediaLibrarySelection, MediaMissionFailure, } from 'lib/types/media-types.js'; import { authoritativeKeyserverID } from '../authoritative-keyserver.js'; import CommIcon from '../components/comm-icon.react.js'; import SWMansionIcon from '../components/swmansion-icon.react.js'; import { encryptMedia } from '../media/encryption-utils.js'; import { getCompatibleMediaURI } from '../media/identifier-utils.js'; import type { MediaResult } from '../media/media-utils.js'; import { processMedia } from '../media/media-utils.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; import Alert from '../utils/alert.js'; import blobServiceUploadHandler from '../utils/blob-service-upload.js'; import { useStaffCanSee } from '../utils/staff-utils.js'; // TODO: flip the switch const useBlobServiceUploads = false; function displayAvatarUpdateFailureAlert(): void { Alert.alert( 'Couldn’t save avatar', 'Please try again later', [{ text: 'OK' }], { cancelable: true }, ); } -function useUploadProcessedMedia(): MediaResult => Promise { +function useUploadProcessedMedia(): ( + media: MediaResult, + metadataUploadLocation: 'keyserver' | 'none', +) => Promise { const callUploadMultimedia = useLegacyAshoatKeyserverCall(uploadMultimedia); const callBlobServiceUpload = useBlobServiceUpload(); - const uploadProcessedMultimedia: MediaResult => Promise = - React.useCallback( - async processedMedia => { - if (!useBlobServiceUploads) { - const { uploadURI, filename, mime, dimensions } = processedMedia; - const { id } = await callUploadMultimedia( - { - uri: uploadURI, - name: filename, - type: mime, - }, - dimensions, - ); - if (!id) { - return undefined; - } - return { type: 'image', uploadID: id }; + return React.useCallback( + async (processedMedia, metadataUploadLocation) => { + const useBlobService = + metadataUploadLocation !== 'keyserver' || useBlobServiceUploads; + if (!useBlobService) { + const { uploadURI, filename, mime, dimensions } = processedMedia; + const { id } = await callUploadMultimedia( + { + uri: uploadURI, + name: filename, + type: mime, + }, + dimensions, + ); + if (!id) { + return undefined; } + return { type: 'image', uploadID: id }; + } - const { result: encryptionResult } = await encryptMedia(processedMedia); - if (!encryptionResult.success) { - throw new Error('Avatar media encryption failed.'); - } + const { result: encryptionResult } = await encryptMedia(processedMedia); + if (!encryptionResult.success) { + throw new Error('Avatar media encryption failed.'); + } - invariant( - encryptionResult.mediaType === 'encrypted_photo', - 'Invalid mediaType after encrypting avatar', - ); - const { - uploadURI, - filename, - mime, + invariant( + encryptionResult.mediaType === 'encrypted_photo', + 'Invalid mediaType after encrypting avatar', + ); + const { + uploadURI, + filename, + mime, + blobHash, + encryptionKey, + dimensions, + thumbHash, + } = encryptionResult; + const { id, uri } = await callBlobServiceUpload({ + uploadInput: { + blobInput: { + type: 'uri', + uri: uploadURI, + filename, + mimeType: mime, + }, blobHash, encryptionKey, dimensions, thumbHash, - } = encryptionResult; - const { id } = await callBlobServiceUpload({ - uploadInput: { - blobInput: { - type: 'uri', - uri: uploadURI, - filename, - mimeType: mime, - }, - blobHash, - encryptionKey, - dimensions, - thumbHash, - loop: false, - }, - keyserverOrThreadID: authoritativeKeyserverID, - callbacks: { blobServiceUploadHandler }, - }); - if (!id) { - return undefined; - } - return { type: 'encrypted_image', uploadID: id }; - }, - [callUploadMultimedia, callBlobServiceUpload], - ); - return uploadProcessedMultimedia; + loop: false, + }, + keyserverOrThreadID: + metadataUploadLocation === 'keyserver' + ? authoritativeKeyserverID + : null, + callbacks: { blobServiceUploadHandler }, + }); + if (metadataUploadLocation !== 'keyserver') { + return { + type: 'non_keyserver_image', + blobURI: uri, + thumbHash, + encryptionKey, + }; + } + if (!id) { + return undefined; + } + return { type: 'encrypted_image', uploadID: id }; + }, + [callUploadMultimedia, callBlobServiceUpload], + ); } function useProcessSelectedMedia(): NativeMediaSelection => Promise< MediaMissionFailure | MediaResult, > { const hasWiFi = useSelector(state => state.connectivity.hasWiFi); const staffCanSee = useStaffCanSee(); const processSelectedMedia = React.useCallback( async (selection: NativeMediaSelection) => { const { resultPromise } = processMedia(selection, { hasWiFi, finalFileHeaderCheck: staffCanSee, }); return await resultPromise; }, [hasWiFi, staffCanSee], ); return processSelectedMedia; } async function selectFromGallery(): Promise { try { const { assets, canceled } = await ImagePicker.launchImageLibraryAsync({ mediaTypes: ImagePicker.MediaTypeOptions.Images, allowsEditing: true, allowsMultipleSelection: false, quality: 1, }); if (canceled || assets.length === 0) { return undefined; } const asset = assets.pop(); const { width, height, assetId: mediaNativeID } = asset; const assetFilename = asset.fileName || filenameFromPathOrURI(asset.uri) || ''; const uri = getCompatibleMediaURI( asset.uri, extensionFromFilename(assetFilename), ); const currentTime = Date.now(); const selection: MediaLibrarySelection = { step: 'photo_library', dimensions: { height, width }, uri, filename: assetFilename, mediaNativeID, selectTime: currentTime, sendTime: currentTime, retries: 0, }; return selection; } catch (e) { console.log(e); return undefined; } } function useUploadSelectedMedia( setProcessingOrUploadInProgress?: (inProgress: boolean) => mixed, -): (selection: NativeMediaSelection) => Promise { +): ( + selection: NativeMediaSelection, + metadataUploadLocation: 'keyserver' | 'none', +) => Promise { const processSelectedMedia = useProcessSelectedMedia(); const uploadProcessedMedia = useUploadProcessedMedia(); return React.useCallback( - async (selection: NativeMediaSelection) => { + async (selection: NativeMediaSelection, metadataUploadLocation) => { setProcessingOrUploadInProgress?.(true); const urisToBeDisposed: Set = new Set([selection.uri]); let processedMedia; try { processedMedia = await processSelectedMedia(selection); if (processedMedia.uploadURI) { urisToBeDisposed.add(processedMedia.uploadURI); } } catch (e) { urisToBeDisposed.forEach(filesystem.unlink); Alert.alert( 'Media processing failed', 'Unable to process selected media.', ); setProcessingOrUploadInProgress?.(false); return undefined; } if (!processedMedia.success) { urisToBeDisposed.forEach(filesystem.unlink); Alert.alert( 'Media processing failed', 'Unable to process selected media.', ); setProcessingOrUploadInProgress?.(false); return undefined; } - let uploadedMedia: ?AvatarDBContent; + let uploadedMedia: ?UpdateUserAvatarRequest; try { - uploadedMedia = await uploadProcessedMedia(processedMedia); + uploadedMedia = await uploadProcessedMedia( + processedMedia, + metadataUploadLocation, + ); urisToBeDisposed.forEach(filesystem.unlink); + setProcessingOrUploadInProgress?.(false); } catch { urisToBeDisposed.forEach(filesystem.unlink); Alert.alert( 'Media upload failed', 'Unable to upload selected media. Please try again.', ); setProcessingOrUploadInProgress?.(false); return undefined; } return uploadedMedia; }, [ processSelectedMedia, setProcessingOrUploadInProgress, uploadProcessedMedia, ], ); } function useNativeSetUserAvatar(): ( request: UpdateUserAvatarRequest, ) => Promise { const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext must be defined'); const { baseSetUserAvatar, getRegistrationModeEnabled, getRegistrationModeSuccessCallback, } = editUserAvatarContext; const nativeSetUserAvatar = React.useCallback( async (request: UpdateUserAvatarRequest) => { const registrationModeEnabled = getRegistrationModeEnabled(); if (registrationModeEnabled) { const successCallback = getRegistrationModeSuccessCallback(); invariant( successCallback, 'successCallback must be defined if registrationModeEnabled is true', ); successCallback({ needsUpload: false, updateUserAvatarRequest: request, }); return; } try { await baseSetUserAvatar(request); } catch { displayAvatarUpdateFailureAlert(); } }, [ getRegistrationModeEnabled, getRegistrationModeSuccessCallback, baseSetUserAvatar, ], ); return nativeSetUserAvatar; } function useNativeUpdateUserImageAvatar(): ( selection: NativeMediaSelection, ) => Promise { const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext must be defined'); const { baseSetUserAvatar, getRegistrationModeEnabled, getRegistrationModeSuccessCallback, setUserAvatarMediaUploadInProgress, } = editUserAvatarContext; const uploadSelectedMedia = useUploadSelectedMedia( setUserAvatarMediaUploadInProgress, ); const nativeUpdateUserImageAvatar = React.useCallback( async (selection: NativeMediaSelection) => { const registrationModeEnabled = getRegistrationModeEnabled(); if (registrationModeEnabled) { const successCallback = getRegistrationModeSuccessCallback(); invariant( successCallback, 'successCallback must be defined if registrationModeEnabled is true', ); successCallback({ needsUpload: true, mediaSelection: selection, }); return; } - const imageAvatarUpdateRequest = await uploadSelectedMedia(selection); + const imageAvatarUpdateRequest = await uploadSelectedMedia( + selection, + 'keyserver', + ); if (!imageAvatarUpdateRequest) { return; } try { await baseSetUserAvatar(imageAvatarUpdateRequest); } catch { displayAvatarUpdateFailureAlert(); } }, [ getRegistrationModeEnabled, getRegistrationModeSuccessCallback, baseSetUserAvatar, uploadSelectedMedia, ], ); return nativeUpdateUserImageAvatar; } function useSelectFromGalleryAndUpdateUserAvatar(): () => Promise { const nativeUpdateUserImageAvatar = useNativeUpdateUserImageAvatar(); const selectFromGalleryAndUpdateUserAvatar = React.useCallback(async (): Promise => { const selection = await selectFromGallery(); if (!selection) { return; } await nativeUpdateUserImageAvatar(selection); }, [nativeUpdateUserImageAvatar]); return selectFromGalleryAndUpdateUserAvatar; } function useNativeSetThreadAvatar(): ( threadID: string, avatarRequest: UpdateUserAvatarRequest, ) => Promise { const editThreadAvatarContext = React.useContext(EditThreadAvatarContext); invariant(editThreadAvatarContext, 'editThreadAvatarContext must be defined'); const { baseSetThreadAvatar } = editThreadAvatarContext; const nativeSetThreadAvatar = React.useCallback( async ( threadID: string, avatarRequest: UpdateUserAvatarRequest, ): Promise => { try { await baseSetThreadAvatar(threadID, avatarRequest); } catch (e) { displayAvatarUpdateFailureAlert(); throw e; } }, [baseSetThreadAvatar], ); return nativeSetThreadAvatar; } function useNativeUpdateThreadImageAvatar(): ( selection: NativeMediaSelection, threadID: string, ) => Promise { const editThreadAvatarContext = React.useContext(EditThreadAvatarContext); invariant(editThreadAvatarContext, 'editThreadAvatarContext must be defined'); const { baseSetThreadAvatar, updateThreadAvatarMediaUploadInProgress } = editThreadAvatarContext; const uploadSelectedMedia = useUploadSelectedMedia( updateThreadAvatarMediaUploadInProgress, ); const nativeUpdateThreadImageAvatar = React.useCallback( async ( selection: NativeMediaSelection, threadID: string, ): Promise => { - const imageAvatarUpdateRequest = await uploadSelectedMedia(selection); + const imageAvatarUpdateRequest = await uploadSelectedMedia( + selection, + 'keyserver', + ); if (!imageAvatarUpdateRequest) { return; } try { await baseSetThreadAvatar(threadID, imageAvatarUpdateRequest); } catch { displayAvatarUpdateFailureAlert(); } }, [baseSetThreadAvatar, uploadSelectedMedia], ); return nativeUpdateThreadImageAvatar; } function useSelectFromGalleryAndUpdateThreadAvatar(): ( threadID: string, ) => Promise { const nativeUpdateThreadImageAvatar = useNativeUpdateThreadImageAvatar(); const selectFromGalleryAndUpdateThreadAvatar = React.useCallback( async (threadID: string): Promise => { const selection: ?MediaLibrarySelection = await selectFromGallery(); if (!selection) { return; } await nativeUpdateThreadImageAvatar(selection, threadID); }, [nativeUpdateThreadImageAvatar], ); return selectFromGalleryAndUpdateThreadAvatar; } type ShowAvatarActionSheetOptions = { +id: 'emoji' | 'image' | 'camera' | 'ens' | 'cancel' | 'remove', +onPress?: () => mixed, }; function useShowAvatarActionSheet( options: $ReadOnlyArray, ): () => void { options = Platform.OS === 'ios' ? [...options, { id: 'cancel' }] : options; const insets = useSafeAreaInsets(); const { showActionSheetWithOptions } = useActionSheet(); const styles = useStyles(unboundStyles); const showAvatarActionSheet = React.useCallback(() => { const texts = options.map((option: ShowAvatarActionSheetOptions) => { if (option.id === 'emoji') { return 'Select emoji'; } else if (option.id === 'image') { return 'Select image'; } else if (option.id === 'camera') { return 'Open camera'; } else if (option.id === 'ens') { return 'Use ENS avatar'; } else if (option.id === 'remove') { return 'Reset to default'; } else { return 'Cancel'; } }); const cancelButtonIndex = options.findIndex( option => option.id === 'cancel', ); const containerStyle = { paddingBotton: insets.bottom, }; const icons = options.map(option => { if (option.id === 'emoji') { return ( ); } else if (option.id === 'image') { return ( ); } else if (option.id === 'camera') { return ( ); } else if (option.id === 'ens') { return ( ); } else if (option.id === 'remove') { return ( ); } else { return undefined; } }); const onPressAction = (selectedIndex: ?number) => { if ( selectedIndex === null || selectedIndex === undefined || selectedIndex < 0 ) { return; } const option = options[selectedIndex]; if (option.onPress) { option.onPress(); } }; showActionSheetWithOptions( { options: texts, cancelButtonIndex, containerStyle, icons, }, onPressAction, ); }, [ insets.bottom, options, showActionSheetWithOptions, styles.bottomSheetIcon, ]); return showAvatarActionSheet; } const unboundStyles = { bottomSheetIcon: { color: '#000000', }, }; export { displayAvatarUpdateFailureAlert, selectFromGallery, useUploadSelectedMedia, useUploadProcessedMedia, useProcessSelectedMedia, useShowAvatarActionSheet, useSelectFromGalleryAndUpdateUserAvatar, useNativeSetUserAvatar, useNativeUpdateUserImageAvatar, useSelectFromGalleryAndUpdateThreadAvatar, useNativeSetThreadAvatar, useNativeUpdateThreadImageAvatar, };