diff --git a/native/avatars/avatar-hooks.js b/native/avatars/avatar-hooks.js --- a/native/avatars/avatar-hooks.js +++ b/native/avatars/avatar-hooks.js @@ -12,10 +12,6 @@ changeThreadSettingsActionTypes, } from 'lib/actions/thread-actions.js'; import { uploadMultimedia } from 'lib/actions/upload-actions.js'; -import { - updateUserAvatar, - updateUserAvatarActionTypes, -} from 'lib/actions/user-actions.js'; import { extensionFromFilename, filenameFromPathOrURI, @@ -23,7 +19,6 @@ import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors.js'; import type { ImageAvatarDBContent, - ENSAvatarDBContent, UpdateUserAvatarRemoveRequest, } from 'lib/types/avatar-types.js'; import type { SetState } from 'lib/types/hook-types.js'; @@ -192,67 +187,6 @@ ); } -const updateUserAvatarLoadingStatusSelector = createLoadingStatusSelector( - updateUserAvatarActionTypes, -); -function useSelectFromGalleryAndUpdateUserAvatar(): [ - () => Promise, - boolean, -] { - const dispatchActionPromise = useDispatchActionPromise(); - const updateUserAvatarCall = useServerCall(updateUserAvatar); - - const [processingOrUploadInProgress, setProcessingOrUploadInProgress] = - React.useState(false); - - const updateUserAvatarLoadingStatus: LoadingStatus = useSelector( - updateUserAvatarLoadingStatusSelector, - ); - - const inProgress = React.useMemo( - () => - processingOrUploadInProgress || - updateUserAvatarLoadingStatus === 'loading', - [processingOrUploadInProgress, updateUserAvatarLoadingStatus], - ); - - const uploadSelectedMedia = useUploadSelectedMedia( - setProcessingOrUploadInProgress, - ); - - const selectFromGalleryAndUpdateUserAvatar = React.useCallback(async () => { - const selection: ?MediaLibrarySelection = await selectFromGallery(); - - const uploadedMediaID = await uploadSelectedMedia(selection); - - if (!uploadedMediaID) { - return; - } - - const imageAvatarUpdateRequest: ImageAvatarDBContent = { - type: 'image', - uploadID: uploadedMediaID, - }; - - dispatchActionPromise( - updateUserAvatarActionTypes, - (async () => { - setProcessingOrUploadInProgress(false); - try { - return await updateUserAvatarCall(imageAvatarUpdateRequest); - } catch { - Alert.alert('Avatar update failed', 'Unable to update avatar.'); - } - })(), - ); - }, [dispatchActionPromise, updateUserAvatarCall, uploadSelectedMedia]); - - return React.useMemo( - () => [selectFromGalleryAndUpdateUserAvatar, inProgress], - [selectFromGalleryAndUpdateUserAvatar, inProgress], - ); -} - const threadAvatarLoadingStatusSelector = createLoadingStatusSelector( changeThreadSettingsActionTypes, `${changeThreadSettingsActionTypes.started}:avatar`, @@ -327,36 +261,6 @@ ); } -function useRemoveUserAvatar(): [() => void, boolean] { - const dispatchActionPromise = useDispatchActionPromise(); - const updateUserAvatarCall = useServerCall(updateUserAvatar); - const updateUserAvatarLoadingStatus: LoadingStatus = useSelector( - updateUserAvatarLoadingStatusSelector, - ); - - const removeUserAvatar = React.useCallback(() => { - const removeAvatarRequest: UpdateUserAvatarRemoveRequest = { - type: 'remove', - }; - - dispatchActionPromise( - updateUserAvatarActionTypes, - (async () => { - try { - return await updateUserAvatarCall(removeAvatarRequest); - } catch { - Alert.alert('Avatar update failed', 'Unable to update avatar.'); - } - })(), - ); - }, [dispatchActionPromise, updateUserAvatarCall]); - - return React.useMemo( - () => [removeUserAvatar, updateUserAvatarLoadingStatus === 'loading'], - [removeUserAvatar, updateUserAvatarLoadingStatus], - ); -} - function useRemoveThreadAvatar(threadID: string): [() => void, boolean] { const dispatchActionPromise = useDispatchActionPromise(); const changeThreadSettingsCall = useServerCall(changeThreadSettings); @@ -395,37 +299,6 @@ ); } -function useENSUserAvatar(): [() => void, boolean] { - const dispatchActionPromise = useDispatchActionPromise(); - const updateUserAvatarCall = useServerCall(updateUserAvatar); - - const updateUserAvatarLoadingStatus: LoadingStatus = useSelector( - updateUserAvatarLoadingStatusSelector, - ); - - const saveENSUserAvatar = React.useCallback(() => { - const ensAvatarRequest: ENSAvatarDBContent = { - type: 'ens', - }; - - dispatchActionPromise( - updateUserAvatarActionTypes, - (async () => { - try { - return await updateUserAvatarCall(ensAvatarRequest); - } catch { - Alert.alert('Avatar update failed', 'Unable to update avatar.'); - } - })(), - ); - }, [dispatchActionPromise, updateUserAvatarCall]); - - return React.useMemo( - () => [saveENSUserAvatar, updateUserAvatarLoadingStatus === 'loading'], - [saveENSUserAvatar, updateUserAvatarLoadingStatus], - ); -} - type ShowAvatarActionSheetOptions = { +id: 'emoji' | 'image' | 'ens' | 'cancel' | 'remove', +onPress?: () => mixed, @@ -540,12 +413,11 @@ }; export { + selectFromGallery, + useUploadSelectedMedia, useUploadProcessedMedia, useProcessSelectedMedia, useShowAvatarActionSheet, - useSelectFromGalleryAndUpdateUserAvatar, useSelectFromGalleryAndUpdateThreadAvatar, - useRemoveUserAvatar, useRemoveThreadAvatar, - useENSUserAvatar, }; diff --git a/native/avatars/edit-user-avatar-provider.react.js b/native/avatars/edit-user-avatar-provider.react.js new file mode 100644 --- /dev/null +++ b/native/avatars/edit-user-avatar-provider.react.js @@ -0,0 +1,153 @@ +// @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 { + ImageAvatarDBContent, + UpdateUserAvatarRemoveRequest, + ENSAvatarDBContent, +} from 'lib/types/avatar-types.js'; +import type { LoadingStatus } from 'lib/types/loading-types.js'; +import type { MediaLibrarySelection } 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 EditUserAvatarContextType = { + +userAvatarSaveInProgress: boolean, + +selectFromGalleryAndUpdateUserAvatar: () => Promise, + +setENSUserAvatar: () => void, + +removeUserAvatar: () => void, +}; + +const EditUserAvatarContext: React.Context = + React.createContext(); + +const updateUserAvatarLoadingStatusSelector = createLoadingStatusSelector( + updateUserAvatarActionTypes, +); + +type Props = { + +children: React.Node, +}; +function EditUserAvatarProvider(props: Props): React.Node { + const { children } = props; + + const dispatchActionPromise = useDispatchActionPromise(); + + const updateUserAvatarCall = useServerCall(updateUserAvatar); + + const [userAvatarMediaUploadInProgress, setUserAvatarMediaUploadInProgress] = + React.useState(false); + + const updateUserAvatarLoadingStatus: LoadingStatus = useSelector( + updateUserAvatarLoadingStatusSelector, + ); + + const userAvatarSaveInProgress = React.useMemo( + () => + userAvatarMediaUploadInProgress || + updateUserAvatarLoadingStatus === 'loading', + [userAvatarMediaUploadInProgress, updateUserAvatarLoadingStatus], + ); + + const uploadUserAvatarSelectedMedia = useUploadSelectedMedia( + setUserAvatarMediaUploadInProgress, + ); + const selectFromGalleryAndUpdateUserAvatar = React.useCallback(async () => { + const selection: ?MediaLibrarySelection = await selectFromGallery(); + const uploadedMediaID = await uploadUserAvatarSelectedMedia(selection); + + if (!uploadedMediaID) { + return; + } + + const imageAvatarUpdateRequest: ImageAvatarDBContent = { + type: 'image', + uploadID: uploadedMediaID, + }; + + dispatchActionPromise( + updateUserAvatarActionTypes, + (async () => { + setUserAvatarMediaUploadInProgress(false); + try { + return await updateUserAvatarCall(imageAvatarUpdateRequest); + } catch { + Alert.alert('Avatar update failed', 'Unable to update avatar.'); + } + })(), + ); + }, [ + dispatchActionPromise, + updateUserAvatarCall, + uploadUserAvatarSelectedMedia, + ]); + + const setENSUserAvatar = React.useCallback(() => { + const ensAvatarRequest: ENSAvatarDBContent = { + type: 'ens', + }; + + dispatchActionPromise( + updateUserAvatarActionTypes, + (async () => { + try { + return await updateUserAvatarCall(ensAvatarRequest); + } catch { + Alert.alert('Avatar update failed', 'Unable to update avatar.'); + } + })(), + ); + }, [dispatchActionPromise, updateUserAvatarCall]); + + const removeUserAvatar = React.useCallback(() => { + const removeAvatarRequest: UpdateUserAvatarRemoveRequest = { + type: 'remove', + }; + + dispatchActionPromise( + updateUserAvatarActionTypes, + (async () => { + try { + return await updateUserAvatarCall(removeAvatarRequest); + } catch { + Alert.alert('Avatar update failed', 'Unable to update avatar.'); + } + })(), + ); + }, [dispatchActionPromise, updateUserAvatarCall]); + + const context = React.useMemo( + () => ({ + userAvatarSaveInProgress, + selectFromGalleryAndUpdateUserAvatar, + setENSUserAvatar, + removeUserAvatar, + }), + [ + removeUserAvatar, + selectFromGalleryAndUpdateUserAvatar, + setENSUserAvatar, + userAvatarSaveInProgress, + ], + ); + + return ( + + {children} + + ); +} + +export { EditUserAvatarContext, EditUserAvatarProvider }; diff --git a/native/avatars/edit-user-avatar.react.js b/native/avatars/edit-user-avatar.react.js --- a/native/avatars/edit-user-avatar.react.js +++ b/native/avatars/edit-user-avatar.react.js @@ -1,18 +1,15 @@ // @flow +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 { - useENSUserAvatar, - useRemoveUserAvatar, - useSelectFromGalleryAndUpdateUserAvatar, - useShowAvatarActionSheet, -} from './avatar-hooks.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 { useSelector } from '../redux/redux-utils.js'; import { useStyles } from '../themes/colors.js'; @@ -26,6 +23,15 @@ const styles = useStyles(unboundStyles); const { userID, onPressEmojiAvatarFlow, disabled } = props; + const editUserAvatarContext = React.useContext(EditUserAvatarContext); + invariant(editUserAvatarContext, 'editUserAvatarContext should be set'); + const { + userAvatarSaveInProgress, + selectFromGalleryAndUpdateUserAvatar, + setENSUserAvatar, + removeUserAvatar, + } = editUserAvatarContext; + const currentUserInfo = useSelector(state => state.currentUserInfo); const ethAddress = React.useMemo( () => getETHAddressForUserInfo(currentUserInfo), @@ -33,17 +39,6 @@ ); const ensAvatarURI = useENSAvatar(ethAddress); - const [selectFromGalleryAndUpdateUserAvatar, isGalleryAvatarUpdateLoading] = - useSelectFromGalleryAndUpdateUserAvatar(); - - const [saveENSUserAvatar, isENSAvatarUpdateLoading] = useENSUserAvatar(); - const [removeUserAvatar, isRemoveAvatarUpdateLoading] = useRemoveUserAvatar(); - - const isAvatarUpdateInProgress = - isGalleryAvatarUpdateLoading || - isRemoveAvatarUpdateLoading || - isENSAvatarUpdateLoading; - const actionSheetConfig = React.useMemo(() => { const configOptions = [ { id: 'emoji', onPress: onPressEmojiAvatarFlow }, @@ -51,7 +46,7 @@ ]; if (ensAvatarURI) { - configOptions.push({ id: 'ens', onPress: saveENSUserAvatar }); + configOptions.push({ id: 'ens', onPress: setENSUserAvatar }); } configOptions.push({ id: 'remove', onPress: removeUserAvatar }); @@ -61,14 +56,14 @@ ensAvatarURI, onPressEmojiAvatarFlow, removeUserAvatar, - saveENSUserAvatar, selectFromGalleryAndUpdateUserAvatar, + setENSUserAvatar, ]); const showAvatarActionSheet = useShowAvatarActionSheet(actionSheetConfig); let spinner; - if (isAvatarUpdateInProgress) { + if (userAvatarSaveInProgress) { spinner = ( diff --git a/native/root.react.js b/native/root.react.js --- a/native/root.react.js +++ b/native/root.react.js @@ -22,6 +22,7 @@ import { MediaCacheProvider } from 'lib/components/media-cache-provider.react.js'; import { actionLogger } from 'lib/utils/action-logger.js'; +import { EditUserAvatarProvider } from './avatars/edit-user-avatar-provider.react.js'; import ChatContextProvider from './chat/chat-context-provider.react.js'; import { FeatureFlagsProvider } from './components/feature-flags-provider.react.js'; import PersistedStateGate from './components/persisted-state-gate.js'; @@ -256,23 +257,25 @@ - - - - - - {gated} - - - - - {navigation} - - + + + + + + + {gated} + + + + + {navigation} + + +