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 @@ -29,6 +29,7 @@ import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { NativeMediaSelection, + PhotoCapture, MediaLibrarySelection, MediaMissionFailure, UploadMultimediaResult, @@ -464,8 +465,119 @@ ); } +function useSelectFromCameraAndUpdateUserAvatar(): { + +updateUserAvatar: (selection: PhotoCapture) => Promise, + +isCameraAvatarUpdateLoading: boolean, +} { + const dispatchActionPromise = useDispatchActionPromise(); + const updateUserAvatarCall = useServerCall(updateUserAvatar); + + const processSelectedMedia = useProcessSelectedMedia(); + const uploadProcessedMedia = useUploadProcessedMedia(); + + const [processingOrUploadInProgress, setProcessingOrUploadInProgress] = + React.useState(false); + + const updateUserAvatarLoadingStatus: LoadingStatus = useSelector( + updateUserAvatarLoadingStatusSelector, + ); + + const inProgress = React.useMemo( + () => + processingOrUploadInProgress || + updateUserAvatarLoadingStatus === 'loading', + [processingOrUploadInProgress, updateUserAvatarLoadingStatus], + ); + + const selectFromCameraAndUpdateUserAvatar = React.useCallback( + async (selection: PhotoCapture) => { + if (!selection) { + Alert.alert( + 'Media selection failed', + 'Unable to select media from Media Library.', + ); + return; + } + + setProcessingOrUploadInProgress(true); + let processedMedia; + try { + processedMedia = await processSelectedMedia(selection); + } catch (e) { + Alert.alert( + 'Media processing failed', + 'Unable to process selected media.', + ); + setProcessingOrUploadInProgress(false); + return; + } + + if (!processedMedia || !processedMedia.success) { + Alert.alert( + 'Media processing failed', + 'Unable to process selected media.', + ); + setProcessingOrUploadInProgress(false); + return; + } + + let uploadedMedia: ?UploadMultimediaResult; + try { + uploadedMedia = await uploadProcessedMedia(processedMedia); + } catch { + Alert.alert( + 'Media upload failed', + 'Unable to upload selected media. Please try again.', + ); + setProcessingOrUploadInProgress(false); + return; + } + + if (!uploadedMedia) { + Alert.alert( + 'Media upload failed', + 'Unable to upload selected media. Please try again.', + ); + setProcessingOrUploadInProgress(false); + return; + } + + const imageAvatarUpdateRequest: ImageAvatarDBContent = { + type: 'image', + uploadID: uploadedMedia.id, + }; + + dispatchActionPromise( + updateUserAvatarActionTypes, + (async () => { + try { + return await updateUserAvatarCall(imageAvatarUpdateRequest); + } catch { + Alert.alert('Avatar update failed', 'Unable to update avatar.'); + } + })(), + ); + setProcessingOrUploadInProgress(false); + }, + [ + dispatchActionPromise, + processSelectedMedia, + updateUserAvatarCall, + uploadProcessedMedia, + ], + ); + + return React.useMemo( + () => ({ + updateUserAvatar: selectFromCameraAndUpdateUserAvatar, + isCameraAvatarUpdateLoading: inProgress, + }), + [selectFromCameraAndUpdateUserAvatar, inProgress], + ); +} + type ShowAvatarActionSheetOptions = { - +id: 'emoji' | 'image' | 'ens' | 'cancel' | 'remove', + +id: 'emoji' | 'image' | 'camera' | 'ens' | 'cancel' | 'remove', +onPress?: () => mixed, }; function useShowAvatarActionSheet( @@ -483,6 +595,8 @@ return 'Use Emoji'; } else if (option.id === 'image') { return 'Select image'; + } else if (option.id === 'camera') { + return 'Camera'; } else if (option.id === 'ens') { return 'Use ENS Avatar'; } else if (option.id === 'remove') { @@ -517,6 +631,14 @@ style={styles.bottomSheetIcon} /> ); + } else if (option.id === 'camera') { + return ( + + ); } else if (option.id === 'ens') { return ( { + navigation.navigate('UserAvatarCameraModal'); + }, [navigation]); + const [saveENSUserAvatar, isENSAvatarUpdateLoading] = useENSUserAvatar(); const [removeUserAvatar, isRemoveAvatarUpdateLoading] = useRemoveUserAvatar(); const isAvatarUpdateInProgress = isGalleryAvatarUpdateLoading || + isCameraAvatarUpdateLoading || isRemoveAvatarUpdateLoading || isENSAvatarUpdateLoading; @@ -48,6 +58,7 @@ const configOptions = [ { id: 'emoji', onPress: onPressEmojiAvatarFlow }, { id: 'image', onPress: selectFromGalleryAndUpdateUserAvatar }, + { id: 'camera', onPress: navigateToCamera }, ]; if (ensAvatarURI) { @@ -59,6 +70,7 @@ return configOptions; }, [ ensAvatarURI, + navigateToCamera, onPressEmojiAvatarFlow, removeUserAvatar, saveENSUserAvatar, diff --git a/native/media/user-avatar-camera-modal.react.js b/native/media/user-avatar-camera-modal.react.js new file mode 100644 --- /dev/null +++ b/native/media/user-avatar-camera-modal.react.js @@ -0,0 +1,32 @@ +// @flow + +import * as React from 'react'; + +import type { PhotoCapture } from 'lib/types/media-types.js'; + +import { useSelectFromCameraAndUpdateUserAvatar } from '../avatars/avatar-hooks.js'; +import CameraModal from '../media/camera-modal.react.js'; +import type { AppNavigationProp } from '../navigation/app-navigator.react.js'; +import type { NavigationRoute } from '../navigation/route-names.js'; + +type Props = { + +navigation: AppNavigationProp<'UserAvatarCameraModal'>, + +route: NavigationRoute<'UserAvatarCameraModal'>, +}; + +function UserAvatarCameraModal(props: Props): React.Node { + const { navigation } = props; + + const { updateUserAvatar } = useSelectFromCameraAndUpdateUserAvatar(); + + const sendPhoto = React.useCallback( + (capture: PhotoCapture) => { + updateUserAvatar(capture); + }, + [updateUserAvatar], + ); + + return ; +} + +export default UserAvatarCameraModal; diff --git a/native/navigation/app-navigator.react.js b/native/navigation/app-navigator.react.js --- a/native/navigation/app-navigator.react.js +++ b/native/navigation/app-navigator.react.js @@ -14,6 +14,7 @@ } from './overlay-navigator.react.js'; import type { RootNavigationProp } from './root-navigator.react.js'; import { + UserAvatarCameraModalRouteName, ImageModalRouteName, MultimediaMessageTooltipModalRouteName, ActionResultModalRouteName, @@ -34,6 +35,7 @@ import KeyboardStateContainer from '../keyboard/keyboard-state-container.react.js'; import ChatCameraModal from '../media/chat-camera-modal.react.js'; import ImageModal from '../media/image-modal.react.js'; +import UserAvatarCameraModal from '../media/user-avatar-camera-modal.react.js'; import VideoPlaybackModal from '../media/video-playback-modal.react.js'; import RelationshipListItemTooltipModal from '../profile/relationship-list-item-tooltip-modal.react.js'; import PushHandler from '../push/push-handler.react.js'; @@ -135,6 +137,10 @@ name={ChatCameraModalRouteName} component={ChatCameraModal} /> +