diff --git a/native/avatars/edit-user-avatar-provider.react.js b/native/avatars/edit-user-avatar-provider.react.js index a53d80046..b87553161 100644 --- a/native/avatars/edit-user-avatar-provider.react.js +++ b/native/avatars/edit-user-avatar-provider.react.js @@ -1,178 +1,181 @@ // @flow import * as React from 'react'; import { Alert } from 'react-native'; import { updateUserAvatar, updateUserAvatarActionTypes, } from 'lib/actions/user-actions.js'; import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { UpdateUserAvatarRequest } from 'lib/types/avatar-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { NativeMediaSelection } from 'lib/types/media-types.js'; import { useDispatchActionPromise, useServerCall, } from 'lib/utils/action-utils.js'; import { selectFromGallery, useUploadSelectedMedia } from './avatar-hooks.js'; import { useSelector } from '../redux/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, +selectFromGalleryAndUpdateUserAvatar: () => Promise, +updateImageUserAvatar: (selection: NativeMediaSelection) => Promise, +setUserAvatar: (avatarRequest: UpdateUserAvatarRequest) => Promise, +setRegistrationMode: (registrationMode: RegistrationMode) => void, - +registrationModeEnabled: boolean, + +getRegistrationModeEnabled: () => boolean, }; const EditUserAvatarContext: React.Context = React.createContext(); const updateUserAvatarLoadingStatusSelector = createLoadingStatusSelector( updateUserAvatarActionTypes, ); const displayFailureAlert = () => Alert.alert( 'Couldn’t save avatar', 'Please try again later', [{ text: 'OK' }], { cancelable: true }, ); type Props = { +children: React.Node, }; function EditUserAvatarProvider(props: Props): React.Node { const { children } = props; - const [registrationMode, setRegistrationMode] = - React.useState(registrationModeOff); + 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 (registrationMode.registrationMode === 'on') { - registrationMode.successCallback({ + 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); try { return await updateUserAvatarCall(imageAvatarUpdateRequest); } catch (e) { displayFailureAlert(); throw e; } })(); dispatchActionPromise(updateUserAvatarActionTypes, promise); await promise; }, - [ - registrationMode, - uploadSelectedMedia, - updateUserAvatarCall, - dispatchActionPromise, - ], + [uploadSelectedMedia, updateUserAvatarCall, dispatchActionPromise], ); const selectFromGalleryAndUpdateUserAvatar = React.useCallback(async () => { const selection = await selectFromGallery(); if (!selection) { return; } await updateImageUserAvatar(selection); }, [updateImageUserAvatar]); const setUserAvatar = React.useCallback( async (request: UpdateUserAvatarRequest) => { - if (registrationMode.registrationMode === 'on') { - registrationMode.successCallback({ + 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(); throw e; } })(); dispatchActionPromise(updateUserAvatarActionTypes, promise); await promise; }, - [registrationMode, updateUserAvatarCall, dispatchActionPromise], + [updateUserAvatarCall, dispatchActionPromise], + ); + + const setRegistrationMode = React.useCallback((mode: RegistrationMode) => { + registrationModeRef.current = mode; + }, []); + const getRegistrationModeEnabled = React.useCallback( + () => registrationModeRef.current.registrationMode === 'on', + [], ); - const registrationModeEnabled = registrationMode.registrationMode === 'on'; const context = React.useMemo( () => ({ userAvatarSaveInProgress, selectFromGalleryAndUpdateUserAvatar, updateImageUserAvatar, setUserAvatar, setRegistrationMode, - registrationModeEnabled, + getRegistrationModeEnabled, }), [ userAvatarSaveInProgress, selectFromGalleryAndUpdateUserAvatar, updateImageUserAvatar, setUserAvatar, setRegistrationMode, - registrationModeEnabled, + getRegistrationModeEnabled, ], ); return ( {children} ); } export { EditUserAvatarContext, EditUserAvatarProvider }; diff --git a/native/avatars/edit-user-avatar.react.js b/native/avatars/edit-user-avatar.react.js index 52e009d86..68d25204e 100644 --- a/native/avatars/edit-user-avatar.react.js +++ b/native/avatars/edit-user-avatar.react.js @@ -1,152 +1,152 @@ // @flow import { useNavigation } from '@react-navigation/native'; import invariant from 'invariant'; import * as React from 'react'; import { ActivityIndicator, TouchableOpacity, View } from 'react-native'; 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 { useShowAvatarActionSheet } from './avatar-hooks.js'; import EditAvatarBadge from './edit-avatar-badge.react.js'; import { EditUserAvatarContext } from './edit-user-avatar-provider.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, selectFromGalleryAndUpdateUserAvatar, setUserAvatar, - registrationModeEnabled, + getRegistrationModeEnabled, } = editUserAvatarContext; 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 (!registrationModeEnabled) { + if (!getRegistrationModeEnabled()) { navigate(EmojiUserAvatarCreationRouteName); return; } navigate<'EmojiAvatarSelection'>({ name: EmojiAvatarSelectionRouteName, params: { usernameOrEthAddress }, }); - }, [navigate, registrationModeEnabled, usernameOrEthAddress]); + }, [navigate, getRegistrationModeEnabled, usernameOrEthAddress]); const navigateToCamera = React.useCallback(() => { navigate( - registrationModeEnabled + getRegistrationModeEnabled() ? RegistrationUserAvatarCameraModalRouteName : UserAvatarCameraModalRouteName, ); - }, [navigate, registrationModeEnabled]); + }, [navigate, getRegistrationModeEnabled]); const setENSUserAvatar = React.useCallback(() => { setUserAvatar({ type: 'ens' }); }, [setUserAvatar]); const removeUserAvatar = React.useCallback(() => { setUserAvatar({ type: 'remove' }); }, [setUserAvatar]); 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;