diff --git a/lib/components/base-edit-user-avatar-provider.react.js b/lib/components/base-edit-user-avatar-provider.react.js index 64b0b5009..ad787b126 100644 --- a/lib/components/base-edit-user-avatar-provider.react.js +++ b/lib/components/base-edit-user-avatar-provider.react.js @@ -1,161 +1,154 @@ // @flow import * as React from 'react'; import { updateUserAvatar, updateUserAvatarActionTypes, } from '../actions/user-actions.js'; import { createLoadingStatusSelector } from '../selectors/loading-selectors.js'; import type { ImageAvatarDBContent, UpdateUserAvatarRequest, } from '../types/avatar-types.js'; import type { LoadingStatus } from '../types/loading-types.js'; import type { NativeMediaSelection } from '../types/media-types.js'; import { useDispatchActionPromise, useServerCall, } from '../utils/action-utils.js'; import { useSelector } from '../utils/redux-utils.js'; export type UserAvatarSelection = | { +needsUpload: true, +mediaSelection: NativeMediaSelection } | { +needsUpload: false, +updateUserAvatarRequest: UpdateUserAvatarRequest }; type RegistrationMode = | { +registrationMode: 'off' } | { +registrationMode: 'on', +successCallback: UserAvatarSelection => mixed, }; const registrationModeOff = { registrationMode: 'off' }; export type EditUserAvatarContextType = { +userAvatarSaveInProgress: boolean, +updateImageUserAvatar: (selection: NativeMediaSelection) => Promise, +setUserAvatar: (avatarRequest: UpdateUserAvatarRequest) => Promise, +setRegistrationMode: (registrationMode: RegistrationMode) => void, +getRegistrationModeEnabled: () => boolean, }; const EditUserAvatarContext: React.Context = React.createContext(); const updateUserAvatarLoadingStatusSelector = createLoadingStatusSelector( updateUserAvatarActionTypes, ); type Props = { +displayFailureAlert?: () => mixed, +useUploadSelectedMedia: ( setProcessingOrUploadInProgress?: (inProgress: boolean) => mixed, ) => (selection: NativeMediaSelection) => Promise, +children: React.Node, }; function BaseEditUserAvatarProvider(props: Props): React.Node { - const { displayFailureAlert, useUploadSelectedMedia, children } = props; + const { useUploadSelectedMedia, children } = props; const registrationModeRef = React.useRef(registrationModeOff); const dispatchActionPromise = useDispatchActionPromise(); const updateUserAvatarCall = useServerCall(updateUserAvatar); const [userAvatarMediaUploadInProgress, setUserAvatarMediaUploadInProgress] = React.useState(false); const updateUserAvatarLoadingStatus: LoadingStatus = useSelector( updateUserAvatarLoadingStatusSelector, ); const userAvatarSaveInProgress = userAvatarMediaUploadInProgress || updateUserAvatarLoadingStatus === 'loading'; const uploadSelectedMedia = useUploadSelectedMedia( setUserAvatarMediaUploadInProgress, ); const updateImageUserAvatar = React.useCallback( async (selection: NativeMediaSelection) => { if (registrationModeRef.current.registrationMode === 'on') { registrationModeRef.current.successCallback({ needsUpload: true, mediaSelection: selection, }); return; } const imageAvatarUpdateRequest = await uploadSelectedMedia(selection); if (!imageAvatarUpdateRequest) { return; } const promise = (async () => { setUserAvatarMediaUploadInProgress(false); return await updateUserAvatarCall(imageAvatarUpdateRequest); })(); dispatchActionPromise(updateUserAvatarActionTypes, promise); await promise; }, [uploadSelectedMedia, dispatchActionPromise, updateUserAvatarCall], ); const setUserAvatar = React.useCallback( async (request: UpdateUserAvatarRequest) => { const regMode = registrationModeRef.current; if (regMode.registrationMode === 'on') { regMode.successCallback({ needsUpload: false, updateUserAvatarRequest: request, }); return; } - const promise = (async () => { - try { - return await updateUserAvatarCall(request); - } catch (e) { - displayFailureAlert && displayFailureAlert(); - throw e; - } - })(); + const promise = updateUserAvatarCall(request); dispatchActionPromise(updateUserAvatarActionTypes, promise); await promise; }, - [dispatchActionPromise, updateUserAvatarCall, displayFailureAlert], + [dispatchActionPromise, updateUserAvatarCall], ); const setRegistrationMode = React.useCallback((mode: RegistrationMode) => { registrationModeRef.current = mode; }, []); const getRegistrationModeEnabled = React.useCallback( () => registrationModeRef.current.registrationMode === 'on', [], ); const context = React.useMemo( () => ({ userAvatarSaveInProgress, updateImageUserAvatar, setUserAvatar, setRegistrationMode, getRegistrationModeEnabled, }), [ userAvatarSaveInProgress, updateImageUserAvatar, setUserAvatar, setRegistrationMode, getRegistrationModeEnabled, ], ); return ( {children} ); } export { EditUserAvatarContext, BaseEditUserAvatarProvider }; diff --git a/native/account/registration/emoji-avatar-selection.react.js b/native/account/registration/emoji-avatar-selection.react.js index 703cfa672..d92069bbf 100644 --- a/native/account/registration/emoji-avatar-selection.react.js +++ b/native/account/registration/emoji-avatar-selection.react.js @@ -1,74 +1,76 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text } from 'react-native'; import { EditUserAvatarContext } from 'lib/components/base-edit-user-avatar-provider.react.js'; import { getDefaultAvatar } from 'lib/shared/avatar-utils.js'; import RegistrationContainer from './registration-container.react.js'; import RegistrationContentContainer from './registration-content-container.react.js'; import type { RegistrationNavigationProp } from './registration-navigator.react.js'; +import { useNativeSetUserAvatar } from '../../avatars/avatar-hooks.js'; import EmojiAvatarCreation from '../../avatars/emoji-avatar-creation.react.js'; import type { NavigationRoute } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; export type EmojiAvatarSelectionParams = { +usernameOrEthAddress: string, }; type Props = { +navigation: RegistrationNavigationProp<'EmojiAvatarSelection'>, +route: NavigationRoute<'EmojiAvatarSelection'>, }; function EmojiAvatarSelection(props: Props): React.Node { const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext should be set'); + const { userAvatarSaveInProgress } = editUserAvatarContext; - const { setUserAvatar, userAvatarSaveInProgress } = editUserAvatarContext; + const nativeSetUserAvatar = useNativeSetUserAvatar(); const { usernameOrEthAddress } = props.route.params; const savedEmojiAvatarFunc = React.useCallback( () => getDefaultAvatar(usernameOrEthAddress), [usernameOrEthAddress], ); const { goBack } = props.navigation; const onSuccess = React.useCallback( avatarRequest => { goBack(); - return setUserAvatar(avatarRequest); + return nativeSetUserAvatar(avatarRequest); }, - [goBack, setUserAvatar], + [goBack, nativeSetUserAvatar], ); const styles = useStyles(unboundStyles); return ( Select an emoji ); } const unboundStyles = { scrollViewContentContainer: { flexGrow: 1, paddingHorizontal: 0, }, header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, paddingHorizontal: 16, }, }; export default EmojiAvatarSelection; diff --git a/native/account/registration/registration-server-call.js b/native/account/registration/registration-server-call.js index d30625069..f373e84e1 100644 --- a/native/account/registration/registration-server-call.js +++ b/native/account/registration/registration-server-call.js @@ -1,236 +1,234 @@ // @flow -import invariant from 'invariant'; import * as React from 'react'; import { Alert, Platform } from 'react-native'; import { useDispatch } from 'react-redux'; import { setDataLoadedActionType } from 'lib/actions/client-db-store-actions.js'; import { registerActionTypes, register } from 'lib/actions/user-actions.js'; -import { EditUserAvatarContext } from 'lib/components/base-edit-user-avatar-provider.react.js'; import type { LogInStartingPayload } from 'lib/types/account-types.js'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils.js'; import { setURLPrefix } from 'lib/utils/url-utils.js'; import type { RegistrationServerCallInput, UsernameAccountSelection, AvatarData, } from './registration-types.js'; -import { useUploadSelectedMedia } from '../../avatars/avatar-hooks.js'; +import { + useNativeSetUserAvatar, + useUploadSelectedMedia, +} from '../../avatars/avatar-hooks.js'; import { NavContext } from '../../navigation/navigation-context.js'; import { useSelector } from '../../redux/redux-utils.js'; import { nativeLogInExtraInfoSelector } from '../../selectors/account-selectors.js'; import { setNativeCredentials } from '../native-credentials.js'; import { useSIWEServerCall } 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: 'waiting_for_registration_call', +avatarData: ?AvatarData, +resolve: () => void, +reject: Error => void, }; const inactiveStep = { step: 'inactive' }; function useRegistrationServerCall(): RegistrationServerCallInput => Promise { const [currentStep, setCurrentStep] = React.useState(inactiveStep); // STEP 1: ACCOUNT REGISTRATION const navContext = React.useContext(NavContext); const logInExtraInfo = useSelector(state => nativeLogInExtraInfoSelector({ redux: state, navContext, }), ); const dispatchActionPromise = useDispatchActionPromise(); const callRegister = useServerCall(register); const registerUsernameAccount = React.useCallback( async ( accountSelection: UsernameAccountSelection, keyserverURL: string, ) => { const extraInfo = await logInExtraInfo(); const registerPromise = (async () => { try { const result = await callRegister( { ...extraInfo, username: accountSelection.username, password: accountSelection.password, }, { urlPrefixOverride: keyserverURL, }, ); await setNativeCredentials({ username: result.currentUserInfo.username, password: accountSelection.password, }); return result; } catch (e) { if (e.message === 'username_reserved') { Alert.alert( 'Username reserved', 'This username is currently reserved. Please contact support@' + 'comm.app if you would like to claim this account.', ); } else if (e.message === 'username_taken') { Alert.alert( 'Username taken', 'An account with that username already exists', ); } else if (e.message === 'client_version_unsupported') { const app = Platform.select({ ios: 'App Store', android: 'Play Store', }); Alert.alert( 'App out of date', 'Your app version is pretty old, and the server doesn’t know how ' + `to speak to it anymore. Please use the ${app} app to update!`, ); } else { Alert.alert('Unknown error', 'Uhh... try again?'); } throw e; } })(); dispatchActionPromise( registerActionTypes, registerPromise, undefined, ({ calendarQuery: extraInfo.calendarQuery }: LogInStartingPayload), ); await registerPromise; }, [logInExtraInfo, callRegister, dispatchActionPromise], ); const siweServerCallParams = React.useMemo(() => { const onServerCallFailure = () => { Alert.alert('Unknown error', 'Uhh... try again?'); }; return { onFailure: onServerCallFailure }; }, []); const siweServerCall = useSIWEServerCall(siweServerCallParams); 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 } = input; if (accountSelection.accountType === 'username') { await registerUsernameAccount(accountSelection, keyserverURL); } else { await siweServerCall(accountSelection, { urlPrefixOverride: keyserverURL, }); } dispatch({ type: setURLPrefix, payload: keyserverURL, }); setCurrentStep({ step: 'waiting_for_registration_call', avatarData, resolve, reject, }); } catch (e) { reject(e); } }, ), [currentStep, registerUsernameAccount, siweServerCall, dispatch], ); // STEP 2: SETTING AVATAR const uploadSelectedMedia = useUploadSelectedMedia(); - - const editUserAvatarContext = React.useContext(EditUserAvatarContext); - invariant(editUserAvatarContext, 'editUserAvatarContext should be set'); - const { setUserAvatar } = editUserAvatarContext; + const nativeSetUserAvatar = useNativeSetUserAvatar(); const hasCurrentUserInfo = useSelector( state => !!state.currentUserInfo && !state.currentUserInfo.anonymous, ); const avatarBeingSetRef = React.useRef(false); React.useEffect(() => { if ( !hasCurrentUserInfo || currentStep.step !== 'waiting_for_registration_call' || avatarBeingSetRef.current ) { return; } avatarBeingSetRef.current = true; const { avatarData, resolve } = currentStep; (async () => { try { if (!avatarData) { return; } let updateUserAvatarRequest; if (!avatarData.needsUpload) { ({ updateUserAvatarRequest } = avatarData); } else { const { mediaSelection } = avatarData; updateUserAvatarRequest = await uploadSelectedMedia(mediaSelection); if (!updateUserAvatarRequest) { return; } } - await setUserAvatar(updateUserAvatarRequest); + await nativeSetUserAvatar(updateUserAvatarRequest); } finally { dispatch({ type: setDataLoadedActionType, payload: { dataLoaded: true, }, }); setCurrentStep(inactiveStep); avatarBeingSetRef.current = false; resolve(); } })(); }, [ currentStep, hasCurrentUserInfo, uploadSelectedMedia, - setUserAvatar, + nativeSetUserAvatar, dispatch, ]); return returnedFunc; } export { useRegistrationServerCall }; diff --git a/native/avatars/avatar-hooks.js b/native/avatars/avatar-hooks.js index 951044448..edf7c370d 100644 --- a/native/avatars/avatar-hooks.js +++ b/native/avatars/avatar-hooks.js @@ -1,363 +1,388 @@ // @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 Alert from 'react-native/Libraries/Alert/Alert.js'; import filesystem from 'react-native-fs'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { uploadMultimedia } from 'lib/actions/upload-actions.js'; import { EditUserAvatarContext } from 'lib/components/base-edit-user-avatar-provider.react.js'; import { extensionFromFilename, filenameFromPathOrURI, } from 'lib/media/file-utils.js'; -import type { ImageAvatarDBContent } from 'lib/types/avatar-types.js'; +import type { + ImageAvatarDBContent, + UpdateUserAvatarRequest, +} from 'lib/types/avatar-types.js'; import type { NativeMediaSelection, MediaLibrarySelection, MediaMissionFailure, UploadMultimediaResult, } from 'lib/types/media-types.js'; import { useServerCall } from 'lib/utils/action-utils.js'; import CommIcon from '../components/comm-icon.react.js'; import SWMansionIcon from '../components/swmansion-icon.react.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 { useStaffCanSee } from '../utils/staff-utils.js'; function displayAvatarUpdateFailureAlert(): void { Alert.alert( 'Couldn’t save avatar', 'Please try again later', [{ text: 'OK' }], { cancelable: true }, ); } function useUploadProcessedMedia(): MediaResult => Promise { const callUploadMultimedia = useServerCall(uploadMultimedia); const uploadProcessedMultimedia: MediaResult => Promise = React.useCallback( processedMedia => { const { uploadURI, filename, mime, dimensions } = processedMedia; return callUploadMultimedia( { uri: uploadURI, name: filename, type: mime, }, dimensions, ); }, [callUploadMultimedia], ); return uploadProcessedMultimedia; } 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 { const processSelectedMedia = useProcessSelectedMedia(); const uploadProcessedMedia = useUploadProcessedMedia(); return React.useCallback( async (selection: NativeMediaSelection) => { 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: UploadMultimediaResult; try { uploadedMedia = await uploadProcessedMedia(processedMedia); urisToBeDisposed.forEach(filesystem.unlink); } catch { urisToBeDisposed.forEach(filesystem.unlink); Alert.alert( 'Media upload failed', 'Unable to upload selected media. Please try again.', ); setProcessingOrUploadInProgress?.(false); return undefined; } if (!uploadedMedia.id) { return undefined; } return { type: 'image', uploadID: uploadedMedia.id, }; }, [ processSelectedMedia, setProcessingOrUploadInProgress, uploadProcessedMedia, ], ); } +function useNativeSetUserAvatar(): ( + request: UpdateUserAvatarRequest, +) => Promise { + const editUserAvatarContext = React.useContext(EditUserAvatarContext); + invariant(editUserAvatarContext, 'editUserAvatarContext must be defined'); + const { setUserAvatar } = editUserAvatarContext; + + const nativeSetUserAvatar = React.useCallback( + async (request: UpdateUserAvatarRequest) => { + try { + await setUserAvatar(request); + } catch { + displayAvatarUpdateFailureAlert(); + } + }, + [setUserAvatar], + ); + + return nativeSetUserAvatar; +} + function useNativeUpdateUserImageAvatar(): ( selection: NativeMediaSelection, ) => Promise { const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext must be defined'); const { updateImageUserAvatar } = editUserAvatarContext; const nativeUpdateUserImageAvatar = React.useCallback( async (selection: NativeMediaSelection) => { try { await updateImageUserAvatar(selection); } catch { displayAvatarUpdateFailureAlert(); } }, [updateImageUserAvatar], ); 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; } 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, }; diff --git a/native/avatars/edit-user-avatar.react.js b/native/avatars/edit-user-avatar.react.js index fd29aef90..8f0955d21 100644 --- a/native/avatars/edit-user-avatar.react.js +++ b/native/avatars/edit-user-avatar.react.js @@ -1,157 +1,157 @@ // @flow import { useNavigation } from '@react-navigation/native'; import invariant from 'invariant'; import * as React from 'react'; import { ActivityIndicator, TouchableOpacity, View } from 'react-native'; import { EditUserAvatarContext } from 'lib/components/base-edit-user-avatar-provider.react.js'; import { useENSAvatar } from 'lib/hooks/ens-cache.js'; import { getETHAddressForUserInfo } from 'lib/shared/account-utils.js'; import type { GenericUserInfoWithAvatar } from 'lib/types/avatar-types.js'; import { + useNativeSetUserAvatar, useSelectFromGalleryAndUpdateUserAvatar, useShowAvatarActionSheet, } from './avatar-hooks.js'; import EditAvatarBadge from './edit-avatar-badge.react.js'; import UserAvatar from './user-avatar.react.js'; import { EmojiUserAvatarCreationRouteName, UserAvatarCameraModalRouteName, EmojiAvatarSelectionRouteName, RegistrationUserAvatarCameraModalRouteName, } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; type Props = | { +userID: ?string, +disabled?: boolean } | { +userInfo: ?GenericUserInfoWithAvatar, +disabled?: boolean, +prefetchedAvatarURI: ?string, }; function EditUserAvatar(props: Props): React.Node { const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext should be set'); - const { - userAvatarSaveInProgress, - setUserAvatar, - getRegistrationModeEnabled, - } = editUserAvatarContext; + const { userAvatarSaveInProgress, getRegistrationModeEnabled } = + editUserAvatarContext; + + const nativeSetUserAvatar = useNativeSetUserAvatar(); const selectFromGalleryAndUpdateUserAvatar = useSelectFromGalleryAndUpdateUserAvatar(); const currentUserInfo = useSelector(state => state.currentUserInfo); const userInfoProp = props.userInfo; const userInfo: ?GenericUserInfoWithAvatar = userInfoProp ?? currentUserInfo; const ethAddress = React.useMemo( () => getETHAddressForUserInfo(userInfo), [userInfo], ); const fetchedENSAvatarURI = useENSAvatar(ethAddress); const ensAvatarURI = fetchedENSAvatarURI ?? props.prefetchedAvatarURI; const { navigate } = useNavigation(); const usernameOrEthAddress = userInfo?.username; const navigateToEmojiSelection = React.useCallback(() => { if (!getRegistrationModeEnabled()) { navigate(EmojiUserAvatarCreationRouteName); return; } navigate<'EmojiAvatarSelection'>({ name: EmojiAvatarSelectionRouteName, params: { usernameOrEthAddress }, }); }, [navigate, getRegistrationModeEnabled, usernameOrEthAddress]); const navigateToCamera = React.useCallback(() => { navigate( getRegistrationModeEnabled() ? RegistrationUserAvatarCameraModalRouteName : UserAvatarCameraModalRouteName, ); }, [navigate, getRegistrationModeEnabled]); const setENSUserAvatar = React.useCallback(() => { - setUserAvatar({ type: 'ens' }); - }, [setUserAvatar]); + nativeSetUserAvatar({ type: 'ens' }); + }, [nativeSetUserAvatar]); const removeUserAvatar = React.useCallback(() => { - setUserAvatar({ type: 'remove' }); - }, [setUserAvatar]); + nativeSetUserAvatar({ type: 'remove' }); + }, [nativeSetUserAvatar]); const hasCurrentAvatar = !!userInfo?.avatar; const actionSheetConfig = React.useMemo(() => { const configOptions = [ { id: 'emoji', onPress: navigateToEmojiSelection }, { id: 'image', onPress: selectFromGalleryAndUpdateUserAvatar }, { id: 'camera', onPress: navigateToCamera }, ]; if (ensAvatarURI) { configOptions.push({ id: 'ens', onPress: setENSUserAvatar }); } if (hasCurrentAvatar) { configOptions.push({ id: 'remove', onPress: removeUserAvatar }); } return configOptions; }, [ hasCurrentAvatar, ensAvatarURI, navigateToCamera, navigateToEmojiSelection, removeUserAvatar, setENSUserAvatar, selectFromGalleryAndUpdateUserAvatar, ]); const showAvatarActionSheet = useShowAvatarActionSheet(actionSheetConfig); const styles = useStyles(unboundStyles); let spinner; if (userAvatarSaveInProgress) { spinner = ( ); } const { userID } = props; const userAvatar = userID ? ( ) : ( ); const { disabled } = props; return ( {userAvatar} {spinner} {!disabled ? : null} ); } const unboundStyles = { spinnerContainer: { position: 'absolute', alignItems: 'center', justifyContent: 'center', top: 0, bottom: 0, left: 0, right: 0, }, }; export default EditUserAvatar; diff --git a/native/profile/emoji-user-avatar-creation.react.js b/native/profile/emoji-user-avatar-creation.react.js index 8330e8236..9d4497c1a 100644 --- a/native/profile/emoji-user-avatar-creation.react.js +++ b/native/profile/emoji-user-avatar-creation.react.js @@ -1,41 +1,45 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { EditUserAvatarContext } from 'lib/components/base-edit-user-avatar-provider.react.js'; import { savedEmojiAvatarSelectorForCurrentUser } from 'lib/selectors/user-selectors.js'; +import { useNativeSetUserAvatar } from '../avatars/avatar-hooks.js'; import EmojiAvatarCreation from '../avatars/emoji-avatar-creation.react.js'; import { displayActionResultModal } from '../navigation/action-result-modal.js'; import { useSelector } from '../redux/redux-utils.js'; // eslint-disable-next-line no-unused-vars function EmojiUserAvatarCreation(props: { ... }): React.Node { const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext should be set'); - const { setUserAvatar, userAvatarSaveInProgress } = editUserAvatarContext; + const { userAvatarSaveInProgress } = editUserAvatarContext; + + const nativeSetUserAvatar = useNativeSetUserAvatar(); + const setAvatar = React.useCallback( async avatarRequest => { - const result = await setUserAvatar(avatarRequest); + const result = await nativeSetUserAvatar(avatarRequest); displayActionResultModal('Avatar updated!'); return result; }, - [setUserAvatar], + [nativeSetUserAvatar], ); const savedEmojiAvatarFunc = useSelector( savedEmojiAvatarSelectorForCurrentUser, ); return ( ); } export default EmojiUserAvatarCreation;