diff --git a/lib/components/base-edit-user-avatar-provider.react.js b/lib/components/edit-user-avatar-provider.react.js similarity index 97% rename from lib/components/base-edit-user-avatar-provider.react.js rename to lib/components/edit-user-avatar-provider.react.js index eedefc9b0..cf7f4071b 100644 --- a/lib/components/base-edit-user-avatar-provider.react.js +++ b/lib/components/edit-user-avatar-provider.react.js @@ -1,142 +1,142 @@ // @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 { SetState } from '../types/hook-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 = { +setUserAvatarMediaUploadInProgress: SetState, +userAvatarSaveInProgress: boolean, +updateImageUserAvatar: ( imageAvatarUpdateRequest: ImageAvatarDBContent, ) => Promise, +baseSetUserAvatar: (avatarRequest: UpdateUserAvatarRequest) => Promise, +setRegistrationMode: (registrationMode: RegistrationMode) => void, +getRegistrationModeEnabled: () => boolean, +getRegistrationModeSuccessCallback: () => ?(UserAvatarSelection) => mixed, }; const EditUserAvatarContext: React.Context = React.createContext(); const updateUserAvatarLoadingStatusSelector = createLoadingStatusSelector( updateUserAvatarActionTypes, ); type Props = { +children: React.Node, }; -function BaseEditUserAvatarProvider(props: Props): React.Node { +function EditUserAvatarProvider(props: Props): React.Node { const { 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 updateImageUserAvatar = React.useCallback( async (imageAvatarUpdateRequest: ImageAvatarDBContent) => { const promise = (async () => { setUserAvatarMediaUploadInProgress(false); return await updateUserAvatarCall(imageAvatarUpdateRequest); })(); dispatchActionPromise(updateUserAvatarActionTypes, promise); await promise; }, [dispatchActionPromise, updateUserAvatarCall], ); // NOTE: Do NOT consume `baseSetUserAvatar` directly. // Use platform-specific `[web/native]SetUserAvatar` instead. const baseSetUserAvatar = React.useCallback( async (request: UpdateUserAvatarRequest) => { const promise = updateUserAvatarCall(request); dispatchActionPromise(updateUserAvatarActionTypes, promise); await promise; }, [dispatchActionPromise, updateUserAvatarCall], ); const setRegistrationMode = React.useCallback((mode: RegistrationMode) => { registrationModeRef.current = mode; }, []); const getRegistrationModeEnabled = React.useCallback( () => registrationModeRef.current.registrationMode === 'on', [], ); const getRegistrationModeSuccessCallback = React.useCallback( () => registrationModeRef.current.registrationMode === 'on' ? registrationModeRef.current.successCallback : null, [], ); const context = React.useMemo( () => ({ setUserAvatarMediaUploadInProgress, userAvatarSaveInProgress, updateImageUserAvatar, baseSetUserAvatar, setRegistrationMode, getRegistrationModeEnabled, getRegistrationModeSuccessCallback, }), [ setUserAvatarMediaUploadInProgress, userAvatarSaveInProgress, updateImageUserAvatar, baseSetUserAvatar, setRegistrationMode, getRegistrationModeEnabled, getRegistrationModeSuccessCallback, ], ); return ( {children} ); } -export { EditUserAvatarContext, BaseEditUserAvatarProvider }; +export { EditUserAvatarContext, EditUserAvatarProvider }; diff --git a/native/account/registration/avatar-selection.react.js b/native/account/registration/avatar-selection.react.js index 0b692110a..d230475bc 100644 --- a/native/account/registration/avatar-selection.react.js +++ b/native/account/registration/avatar-selection.react.js @@ -1,203 +1,203 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { Text, View } from 'react-native'; import { EditUserAvatarContext, type UserAvatarSelection, -} from 'lib/components/base-edit-user-avatar-provider.react.js'; +} from 'lib/components/edit-user-avatar-provider.react.js'; import RegistrationButtonContainer from './registration-button-container.react.js'; import RegistrationButton from './registration-button.react.js'; import RegistrationContainer from './registration-container.react.js'; import RegistrationContentContainer from './registration-content-container.react.js'; import { RegistrationContext } from './registration-context.js'; import type { RegistrationNavigationProp } from './registration-navigator.react.js'; import { type CoolOrNerdMode, type AccountSelection, type AvatarData, ensAvatarSelection, } from './registration-types.js'; import EditUserAvatar from '../../avatars/edit-user-avatar.react.js'; import { useCurrentLeafRouteName } from '../../navigation/nav-selectors.js'; import { type NavigationRoute, RegistrationTermsRouteName, AvatarSelectionRouteName, EmojiAvatarSelectionRouteName, RegistrationUserAvatarCameraModalRouteName, } from '../../navigation/route-names.js'; import { useStyles } from '../../themes/colors.js'; export type AvatarSelectionParams = { +userSelections: { +coolOrNerdMode: CoolOrNerdMode, +keyserverURL: string, +accountSelection: AccountSelection, }, }; type Props = { +navigation: RegistrationNavigationProp<'AvatarSelection'>, +route: NavigationRoute<'AvatarSelection'>, }; function AvatarSelection(props: Props): React.Node { const { userSelections } = props.route.params; const { accountSelection } = userSelections; const usernameOrETHAddress = accountSelection.accountType === 'username' ? accountSelection.username : accountSelection.address; const registrationContext = React.useContext(RegistrationContext); invariant(registrationContext, 'registrationContext should be set'); const { cachedSelections, setCachedSelections } = registrationContext; const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext should be set'); const { setRegistrationMode } = editUserAvatarContext; const prefetchedAvatarURI = accountSelection.accountType === 'ethereum' ? accountSelection.avatarURI : undefined; let initialAvatarData = cachedSelections.avatarData; if (!initialAvatarData && prefetchedAvatarURI) { initialAvatarData = ensAvatarSelection; } const [avatarData, setAvatarData] = React.useState(initialAvatarData); const setClientAvatarFromSelection = React.useCallback( (selection: UserAvatarSelection) => { if (selection.needsUpload) { const newAvatarData = { ...selection, clientAvatar: { type: 'image', uri: selection.mediaSelection.uri, }, }; setAvatarData(newAvatarData); setCachedSelections(oldUserSelections => ({ ...oldUserSelections, avatarData: newAvatarData, })); } else if (selection.updateUserAvatarRequest.type !== 'remove') { const clientRequest = selection.updateUserAvatarRequest; invariant( clientRequest.type !== 'image', 'image avatars need to be uploaded', ); const newAvatarData = { ...selection, clientAvatar: clientRequest, }; setAvatarData(newAvatarData); setCachedSelections(oldUserSelections => ({ ...oldUserSelections, avatarData: newAvatarData, })); } else { setAvatarData(undefined); setCachedSelections(oldUserSelections => ({ ...oldUserSelections, avatarData: undefined, })); } }, [setCachedSelections], ); const currentRouteName = useCurrentLeafRouteName(); const avatarSelectionHappening = currentRouteName === AvatarSelectionRouteName || currentRouteName === EmojiAvatarSelectionRouteName || currentRouteName === RegistrationUserAvatarCameraModalRouteName; React.useEffect(() => { if (!avatarSelectionHappening) { return undefined; } setRegistrationMode({ registrationMode: 'on', successCallback: setClientAvatarFromSelection, }); return () => { setRegistrationMode({ registrationMode: 'off' }); }; }, [ avatarSelectionHappening, setRegistrationMode, setClientAvatarFromSelection, ]); const { navigate } = props.navigation; const onProceed = React.useCallback(async () => { const newUserSelections = { ...userSelections, avatarData, }; navigate<'RegistrationTerms'>({ name: RegistrationTermsRouteName, params: { userSelections: newUserSelections }, }); }, [userSelections, avatarData, navigate]); const clientAvatar = avatarData?.clientAvatar; const userInfoOverride = React.useMemo( () => ({ username: usernameOrETHAddress, avatar: clientAvatar, }), [usernameOrETHAddress, clientAvatar], ); const styles = useStyles(unboundStyles); return ( Pick an avatar ); } const unboundStyles = { scrollViewContentContainer: { paddingHorizontal: 0, }, header: { fontSize: 24, color: 'panelForegroundLabel', paddingBottom: 16, paddingHorizontal: 16, }, stagedAvatarSection: { marginTop: 16, backgroundColor: 'panelForeground', paddingVertical: 24, alignItems: 'center', }, editUserAvatar: { alignItems: 'center', justifyContent: 'center', }, }; export default AvatarSelection; diff --git a/native/account/registration/emoji-avatar-selection.react.js b/native/account/registration/emoji-avatar-selection.react.js index d92069bbf..63325088b 100644 --- a/native/account/registration/emoji-avatar-selection.react.js +++ b/native/account/registration/emoji-avatar-selection.react.js @@ -1,76 +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 { EditUserAvatarContext } from 'lib/components/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 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 nativeSetUserAvatar(avatarRequest); }, [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/avatars/avatar-hooks.js b/native/avatars/avatar-hooks.js index 29620b364..021e63386 100644 --- a/native/avatars/avatar-hooks.js +++ b/native/avatars/avatar-hooks.js @@ -1,443 +1,443 @@ // @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 { EditUserAvatarContext } from 'lib/components/edit-user-avatar-provider.react.js'; import { extensionFromFilename, filenameFromPathOrURI, } from 'lib/media/file-utils.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 { 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 { updateImageUserAvatar, 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); if (!imageAvatarUpdateRequest) { return; } try { await updateImageUserAvatar(imageAvatarUpdateRequest); } catch { displayAvatarUpdateFailureAlert(); } }, [ getRegistrationModeEnabled, getRegistrationModeSuccessCallback, updateImageUserAvatar, 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; } 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 8f0955d21..d1880f8d3 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 { EditUserAvatarContext } from 'lib/components/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, 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(() => { nativeSetUserAvatar({ type: 'ens' }); }, [nativeSetUserAvatar]); const removeUserAvatar = React.useCallback(() => { 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 9d4497c1a..0a7fe427e 100644 --- a/native/profile/emoji-user-avatar-creation.react.js +++ b/native/profile/emoji-user-avatar-creation.react.js @@ -1,45 +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 { EditUserAvatarContext } from 'lib/components/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 { userAvatarSaveInProgress } = editUserAvatarContext; const nativeSetUserAvatar = useNativeSetUserAvatar(); const setAvatar = React.useCallback( async avatarRequest => { const result = await nativeSetUserAvatar(avatarRequest); displayActionResultModal('Avatar updated!'); return result; }, [nativeSetUserAvatar], ); const savedEmojiAvatarFunc = useSelector( savedEmojiAvatarSelectorForCurrentUser, ); return ( ); } export default EmojiUserAvatarCreation; diff --git a/native/root.react.js b/native/root.react.js index 24582d1b1..72b950f7b 100644 --- a/native/root.react.js +++ b/native/root.react.js @@ -1,328 +1,328 @@ // @flow import { ActionSheetProvider } from '@expo/react-native-action-sheet'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { useReduxDevToolsExtension } from '@react-navigation/devtools'; import { NavigationContainer } from '@react-navigation/native'; import type { PossiblyStaleNavigationState } from '@react-navigation/native'; import * as SplashScreen from 'expo-splash-screen'; import invariant from 'invariant'; import * as React from 'react'; import { Platform, UIManager, StyleSheet } from 'react-native'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import Orientation from 'react-native-orientation-locker'; import { SafeAreaProvider, initialWindowMetrics, } from 'react-native-safe-area-context'; import { Provider } from 'react-redux'; import { PersistGate as ReduxPersistGate } from 'redux-persist/es/integration/react.js'; -import { BaseEditUserAvatarProvider } from 'lib/components/base-edit-user-avatar-provider.react.js'; +import { EditUserAvatarProvider } from 'lib/components/edit-user-avatar-provider.react.js'; import { ENSCacheProvider } from 'lib/components/ens-cache-provider.react.js'; import { MediaCacheProvider } from 'lib/components/media-cache-provider.react.js'; import { actionLogger } from 'lib/utils/action-logger.js'; import { RegistrationContextProvider } from './account/registration/registration-context-provider.react.js'; import NativeEditThreadAvatarProvider from './avatars/native-edit-thread-avatar-provider.react.js'; import ChatContextProvider from './chat/chat-context-provider.react.js'; import MessageEditingContextProvider from './chat/message-editing-context-provider.react.js'; import { FeatureFlagsProvider } from './components/feature-flags-provider.react.js'; import PersistedStateGate from './components/persisted-state-gate.js'; import ConnectedStatusBar from './connected-status-bar.react.js'; import { SQLiteDataHandler } from './data/sqlite-data-handler.js'; import ErrorBoundary from './error-boundary.react.js'; import InputStateContainer from './input/input-state-container.react.js'; import { InviteLinksContextProvider } from './invite-links/invite-links-context-provider.react.js'; import LifecycleHandler from './lifecycle/lifecycle-handler.react.js'; import MarkdownContextProvider from './markdown/markdown-context-provider.react.js'; import { filesystemMediaCache } from './media/media-cache.js'; import { defaultNavigationState } from './navigation/default-state.js'; import DisconnectedBarVisibilityHandler from './navigation/disconnected-bar-visibility-handler.react.js'; import { setGlobalNavContext } from './navigation/icky-global.js'; import { NavContext } from './navigation/navigation-context.js'; import NavigationHandler from './navigation/navigation-handler.react.js'; import { validNavState } from './navigation/navigation-utils.js'; import OrientationHandler from './navigation/orientation-handler.react.js'; import { navStateAsyncStorageKey } from './navigation/persistance.js'; import RootNavigator from './navigation/root-navigator.react.js'; import ConnectivityUpdater from './redux/connectivity-updater.react.js'; import { DimensionsUpdater } from './redux/dimensions-updater.react.js'; import { getPersistor } from './redux/persist.js'; import { store } from './redux/redux-setup.js'; import { useSelector } from './redux/redux-utils.js'; import { RootContext } from './root-context.js'; import { MessageSearchProvider } from './search/search-provider.react.js'; import Socket from './socket.react.js'; import { StaffContextProvider } from './staff/staff-context.provider.react.js'; import { useLoadCommFonts } from './themes/fonts.js'; import { DarkTheme, LightTheme } from './themes/navigation.js'; import ThemeHandler from './themes/theme-handler.react.js'; import { provider } from './utils/ethers-utils.js'; // Add custom items to expo-dev-menu import './dev-menu.js'; import './types/message-types-validator.js'; if (Platform.OS === 'android') { UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true); } const navInitAction = Object.freeze({ type: 'NAV/@@INIT' }); const navUnknownAction = Object.freeze({ type: 'NAV/@@UNKNOWN' }); SplashScreen.preventAutoHideAsync().catch(console.log); function Root() { const navStateRef = React.useRef(); const navDispatchRef = React.useRef(); const navStateInitializedRef = React.useRef(false); // We call this here to start the loading process // We gate the UI on the fonts loading in AppNavigator useLoadCommFonts(); const [navContext, setNavContext] = React.useState(null); const updateNavContext = React.useCallback(() => { if ( !navStateRef.current || !navDispatchRef.current || !navStateInitializedRef.current ) { return; } const updatedNavContext = { state: navStateRef.current, dispatch: navDispatchRef.current, }; setNavContext(updatedNavContext); setGlobalNavContext(updatedNavContext); }, []); const [initialState, setInitialState] = React.useState( __DEV__ ? undefined : defaultNavigationState, ); React.useEffect(() => { Orientation.lockToPortrait(); (async () => { let loadedState = initialState; if (__DEV__) { try { const navStateString = await AsyncStorage.getItem( navStateAsyncStorageKey, ); if (navStateString) { const savedState = JSON.parse(navStateString); if (validNavState(savedState)) { loadedState = savedState; } } } catch {} } if (!loadedState) { loadedState = defaultNavigationState; } if (loadedState !== initialState) { setInitialState(loadedState); } navStateRef.current = loadedState; updateNavContext(); actionLogger.addOtherAction('navState', navInitAction, null, loadedState); })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [updateNavContext]); const setNavStateInitialized = React.useCallback(() => { navStateInitializedRef.current = true; updateNavContext(); }, [updateNavContext]); const [rootContext, setRootContext] = React.useState(() => ({ setNavStateInitialized, })); const detectUnsupervisedBackgroundRef = React.useCallback( (detectUnsupervisedBackground: ?(alreadyClosed: boolean) => boolean) => { setRootContext(prevRootContext => ({ ...prevRootContext, detectUnsupervisedBackground, })); }, [], ); const frozen = useSelector(state => state.frozen); const queuedActionsRef = React.useRef([]); const onNavigationStateChange = React.useCallback( (state: ?PossiblyStaleNavigationState) => { invariant(state, 'nav state should be non-null'); const prevState = navStateRef.current; navStateRef.current = state; updateNavContext(); const queuedActions = queuedActionsRef.current; queuedActionsRef.current = []; if (queuedActions.length === 0) { queuedActions.push(navUnknownAction); } for (const action of queuedActions) { actionLogger.addOtherAction('navState', action, prevState, state); } if (!__DEV__ || frozen) { return; } (async () => { try { await AsyncStorage.setItem( navStateAsyncStorageKey, JSON.stringify(state), ); } catch (e) { console.log('AsyncStorage threw while trying to persist navState', e); } })(); }, [updateNavContext, frozen], ); const navContainerRef = React.useRef(); const containerRef = React.useCallback( (navContainer: ?React.ElementRef) => { navContainerRef.current = navContainer; if (navContainer && !navDispatchRef.current) { navDispatchRef.current = navContainer.dispatch; updateNavContext(); } }, [updateNavContext], ); useReduxDevToolsExtension(navContainerRef); const navContainer = navContainerRef.current; React.useEffect(() => { if (!navContainer) { return undefined; } return navContainer.addListener('__unsafe_action__', event => { const { action, noop } = event.data; const navState = navStateRef.current; if (noop) { actionLogger.addOtherAction('navState', action, navState, navState); return; } queuedActionsRef.current.push({ ...action, type: `NAV/${action.type}`, }); }); }, [navContainer]); const activeTheme = useSelector(state => state.globalThemeInfo.activeTheme); const theme = (() => { if (activeTheme === 'light') { return LightTheme; } else if (activeTheme === 'dark') { return DarkTheme; } return undefined; })(); const gated: React.Node = ( <> ); let navigation; if (initialState) { navigation = ( ); } return ( - + {gated} {navigation} - + ); } const styles = StyleSheet.create({ app: { flex: 1, }, }); function AppRoot(): React.Node { return ( ); } export default AppRoot; diff --git a/web/app.react.js b/web/app.react.js index ad9fb42ab..3fd3b616a 100644 --- a/web/app.react.js +++ b/web/app.react.js @@ -1,376 +1,376 @@ // @flow import 'basscss/css/basscss.min.css'; import './theme.css'; import { config as faConfig } from '@fortawesome/fontawesome-svg-core'; import classnames from 'classnames'; import _isEqual from 'lodash/fp/isEqual.js'; import * as React from 'react'; import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { useDispatch } from 'react-redux'; import { WagmiConfig } from 'wagmi'; import { fetchEntriesActionTypes, updateCalendarQueryActionTypes, } from 'lib/actions/entry-actions.js'; -import { BaseEditUserAvatarProvider } from 'lib/components/base-edit-user-avatar-provider.react.js'; +import { EditUserAvatarProvider } from 'lib/components/edit-user-avatar-provider.react.js'; import { ModalProvider, useModalContext, } from 'lib/components/modal-provider.react.js'; import { webAndKeyserverCodeVersion } from 'lib/facts/version.js'; import { createLoadingStatusSelector, combineLoadingStatuses, } from 'lib/selectors/loading-selectors.js'; import { isLoggedIn } from 'lib/selectors/user-selectors.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import { registerConfig } from 'lib/utils/config.js'; import { infoFromURL } from 'lib/utils/url-utils.js'; import { WagmiENSCacheProvider, wagmiClient } from 'lib/utils/wagmi-utils.js'; import Calendar from './calendar/calendar.react.js'; import Chat from './chat/chat.react.js'; import { EditModalProvider } from './chat/edit-message-provider.js'; import { TooltipProvider } from './chat/tooltip-provider.js'; import NavigationArrows from './components/navigation-arrows.react.js'; import { initOpaque } from './crypto/opaque-utils.js'; import electron from './electron.js'; import InputStateContainer from './input/input-state-container.react.js'; import InviteLinkHandler from './invite-links/invite-link-handler.react.js'; import InviteLinksRefresher from './invite-links/invite-links-refresher.react.js'; import LoadingIndicator from './loading-indicator.react.js'; import { MenuProvider } from './menu-provider.react.js'; import UpdateModalHandler from './modals/update-modal.react.js'; import SettingsSwitcher from './navigation-panels/settings-switcher.react.js'; import Topbar from './navigation-panels/topbar.react.js'; import useBadgeHandler from './push-notif/badge-handler.react.js'; import { PushNotificationsHandler } from './push-notif/push-notifs-handler.js'; import { updateNavInfoActionType } from './redux/action-types.js'; import DeviceIDUpdater from './redux/device-id-updater.js'; import DisconnectedBarVisibilityHandler from './redux/disconnected-bar-visibility-handler.js'; import DisconnectedBar from './redux/disconnected-bar.js'; import FocusHandler from './redux/focus-handler.react.js'; import { persistConfig } from './redux/persist.js'; import PolicyAcknowledgmentHandler from './redux/policy-acknowledgment-handler.js'; import { useSelector } from './redux/redux-utils.js'; import VisibilityHandler from './redux/visibility-handler.react.js'; import history from './router-history.js'; import { MessageSearchStateProvider } from './search/message-search-state-provider.react.js'; import AccountSettings from './settings/account-settings.react.js'; import DangerZone from './settings/danger-zone.react.js'; import CommunityPicker from './sidebar/community-picker.react.js'; import Splash from './splash/splash.react.js'; import './typography.css'; import css from './style.css'; import { type NavInfo } from './types/nav-types.js'; import { canonicalURLFromReduxState, navInfoFromURL } from './url-utils.js'; initOpaque(); // We want Webpack's css-loader and style-loader to handle the Fontawesome CSS, // so we disable the autoAddCss logic and import the CSS file. Otherwise every // icon flashes huge for a second before the CSS is loaded. import '@fortawesome/fontawesome-svg-core/styles.css'; faConfig.autoAddCss = false; registerConfig({ // We can't securely cache credentials on web, so we have no way to recover // from a cookie invalidation resolveInvalidatedCookie: null, // We use httponly cookies on web to protect against XSS attacks, so we have // no access to the cookies from JavaScript setCookieOnRequest: false, setSessionIDOnRequest: true, // Never reset the calendar range calendarRangeInactivityLimit: null, platformDetails: { platform: electron?.platform ?? 'web', codeVersion: webAndKeyserverCodeVersion, stateVersion: persistConfig.version, }, }); type BaseProps = { +location: { +pathname: string, ... }, }; type Props = { ...BaseProps, // Redux state +navInfo: NavInfo, +entriesLoadingStatus: LoadingStatus, +loggedIn: boolean, +activeThreadCurrentlyUnread: boolean, // Redux dispatch functions +dispatch: Dispatch, +modals: $ReadOnlyArray, }; class App extends React.PureComponent { componentDidMount() { const { navInfo, location: { pathname }, loggedIn, } = this.props; const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn); if (pathname !== newURL) { history.replace(newURL); } } componentDidUpdate(prevProps: Props) { const { navInfo, location: { pathname }, loggedIn, } = this.props; if (!_isEqual(navInfo)(prevProps.navInfo)) { const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn); if (newURL !== pathname) { history.push(newURL); } } else if (pathname !== prevProps.location.pathname) { const urlInfo = infoFromURL(pathname); const newNavInfo = navInfoFromURL(urlInfo, { navInfo }); if (!_isEqual(newNavInfo)(navInfo)) { this.props.dispatch({ type: updateNavInfoActionType, payload: newNavInfo, }); } } else if (loggedIn !== prevProps.loggedIn) { const newURL = canonicalURLFromReduxState(navInfo, pathname, loggedIn); if (newURL !== pathname) { history.replace(newURL); } } if (loggedIn !== prevProps.loggedIn) { electron?.clearHistory(); } } onWordmarkClicked = () => { this.props.dispatch({ type: updateNavInfoActionType, payload: { tab: 'chat' }, }); }; render() { let content; if (this.props.loggedIn) { content = ( <> - + {this.renderMainContent()} {this.props.modals} - + ); } else { content = ( <> {this.props.modals} ); } return ( {content} ); } onHeaderDoubleClick = () => electron?.doubleClickTopBar(); stopDoubleClickPropagation = electron ? e => e.stopPropagation() : null; renderMainContent() { const mainContent = this.getMainContentWithSwitcher(); let navigationArrows = null; if (electron) { navigationArrows = ; } const headerClasses = classnames({ [css.header]: true, [css['electron-draggable']]: electron, }); const wordmarkClasses = classnames({ [css.wordmark]: true, [css['electron-non-draggable']]: electron, [css['wordmark-macos']]: electron?.platform === 'macos', }); return (

Comm

{navigationArrows}
{mainContent}
); } getMainContentWithSwitcher() { const { tab, settingsSection } = this.props.navInfo; let mainContent; if (tab === 'settings') { if (settingsSection === 'account') { mainContent = ; } else if (settingsSection === 'danger-zone') { mainContent = ; } return (
{mainContent}
); } if (tab === 'calendar') { mainContent = ; } else if (tab === 'chat') { mainContent = ; } const mainContentClass = classnames( css['main-content-container'], css['main-content-container-column'], ); return (
{mainContent}
); } } const fetchEntriesLoadingStatusSelector = createLoadingStatusSelector( fetchEntriesActionTypes, ); const updateCalendarQueryLoadingStatusSelector = createLoadingStatusSelector( updateCalendarQueryActionTypes, ); const ConnectedApp: React.ComponentType = React.memo( function ConnectedApp(props) { const activeChatThreadID = useSelector( state => state.navInfo.activeChatThreadID, ); const navInfo = useSelector(state => state.navInfo); const fetchEntriesLoadingStatus = useSelector( fetchEntriesLoadingStatusSelector, ); const updateCalendarQueryLoadingStatus = useSelector( updateCalendarQueryLoadingStatusSelector, ); const entriesLoadingStatus = combineLoadingStatuses( fetchEntriesLoadingStatus, updateCalendarQueryLoadingStatus, ); const loggedIn = useSelector(isLoggedIn); const activeThreadCurrentlyUnread = useSelector( state => !activeChatThreadID || !!state.threadStore.threadInfos[activeChatThreadID]?.currentUser.unread, ); useBadgeHandler(); const dispatch = useDispatch(); const modalContext = useModalContext(); const modals = React.useMemo( () => modalContext.modals.map(([modal, key]) => ( {modal} )), [modalContext.modals], ); return ( ); }, ); function AppWithProvider(props: BaseProps): React.Node { return ( ); } export default AppWithProvider; diff --git a/web/avatars/edit-user-avatar-menu.react.js b/web/avatars/edit-user-avatar-menu.react.js index 3ebcae545..2c6cf9f22 100644 --- a/web/avatars/edit-user-avatar-menu.react.js +++ b/web/avatars/edit-user-avatar-menu.react.js @@ -1,105 +1,105 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; -import { EditUserAvatarContext } from 'lib/components/base-edit-user-avatar-provider.react.js'; +import { EditUserAvatarContext } from 'lib/components/edit-user-avatar-provider.react.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import css from './edit-user-avatar-menu.css'; import EmojiAvatarSelectionModal from './emoji-avatar-selection-modal.react.js'; import MenuItem from '../components/menu-item.react.js'; import Menu from '../components/menu.react.js'; import { allowedMimeTypeString } from '../media/file-utils.js'; const editIcon = (
); function EditUserAvatarMenu(): React.Node { const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext should be set'); const { baseSetUserAvatar } = editUserAvatarContext; const removeUserAvatar = React.useCallback( () => baseSetUserAvatar({ type: 'remove' }), [baseSetUserAvatar], ); const { pushModal } = useModalContext(); const openEmojiSelectionModal = React.useCallback( () => pushModal(), [pushModal], ); const emojiMenuItem = React.useMemo( () => ( ), [openEmojiSelectionModal], ); const imageInputRef = React.useRef(); const onImageMenuItemClicked = React.useCallback( () => imageInputRef.current?.click(), [], ); const onImageSelected = React.useCallback( event => console.log(event.target.files), [], ); const imageMenuItem = React.useMemo( () => ( ), [onImageMenuItemClicked], ); const removeMenuItem = React.useMemo( () => ( ), [removeUserAvatar], ); const menuItems = React.useMemo( () => [emojiMenuItem, imageMenuItem, removeMenuItem], [emojiMenuItem, imageMenuItem, removeMenuItem], ); return (
{menuItems}
); } export default EditUserAvatarMenu; diff --git a/web/avatars/edit-user-avatar.react.js b/web/avatars/edit-user-avatar.react.js index 13bbe6438..b6f06ae2d 100644 --- a/web/avatars/edit-user-avatar.react.js +++ b/web/avatars/edit-user-avatar.react.js @@ -1,36 +1,36 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; -import { EditUserAvatarContext } from 'lib/components/base-edit-user-avatar-provider.react.js'; +import { EditUserAvatarContext } from 'lib/components/edit-user-avatar-provider.react.js'; import EditUserAvatarMenu from './edit-user-avatar-menu.react.js'; import css from './edit-user-avatar.css'; import UserAvatar from './user-avatar.react.js'; type Props = { +userID: ?string, +disabled?: boolean, }; function EditUserAvatar(props: Props): React.Node { const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext should be set'); const { userAvatarSaveInProgress } = editUserAvatarContext; const { userID } = props; return (
{!userAvatarSaveInProgress ? : null}
); } export default EditUserAvatar; diff --git a/web/avatars/emoji-avatar-selection-modal.react.js b/web/avatars/emoji-avatar-selection-modal.react.js index effb81453..cc2edc9dd 100644 --- a/web/avatars/emoji-avatar-selection-modal.react.js +++ b/web/avatars/emoji-avatar-selection-modal.react.js @@ -1,170 +1,170 @@ // @flow import data from '@emoji-mart/data'; import Picker from '@emoji-mart/react'; import invariant from 'invariant'; import * as React from 'react'; -import { EditUserAvatarContext } from 'lib/components/base-edit-user-avatar-provider.react.js'; +import { EditUserAvatarContext } from 'lib/components/edit-user-avatar-provider.react.js'; import { useModalContext } from 'lib/components/modal-provider.react.js'; import SWMansionIcon from 'lib/components/SWMansionIcon.react.js'; import { defaultAnonymousUserEmojiAvatar, getAvatarForUser, getDefaultAvatar, } from 'lib/shared/avatar-utils.js'; import type { ClientAvatar, ClientEmojiAvatar, } from 'lib/types/avatar-types.js'; import Avatar from './avatar.react.js'; import css from './emoji-avatar-selection-modal.css'; import Button, { buttonThemes } from '../components/button.react.js'; import Tabs from '../components/tabs.react.js'; import LoadingIndicator from '../loading-indicator.react.js'; import Modal from '../modals/modal.react.js'; import ColorSelector from '../modals/threads/color-selector.react.js'; import { useSelector } from '../redux/redux-utils.js'; type TabType = 'emoji' | 'color'; function EmojiAvatarSelectionModal(): React.Node { const { popModal } = useModalContext(); const editUserAvatarContext = React.useContext(EditUserAvatarContext); invariant(editUserAvatarContext, 'editUserAvatarContext should be set'); const { baseSetUserAvatar, userAvatarSaveInProgress } = editUserAvatarContext; const [updateAvatarStatus, setUpdateAvatarStatus] = React.useState(); const currentUserInfo = useSelector(state => state.currentUserInfo); const currentUserAvatar: ClientAvatar = getAvatarForUser(currentUserInfo); const defaultUserAvatar: ClientEmojiAvatar = currentUserInfo?.username ? getDefaultAvatar(currentUserInfo.username) : defaultAnonymousUserEmojiAvatar; // eslint-disable-next-line no-unused-vars const [pendingAvatarEmoji, setPendingAvatarEmoji] = React.useState( currentUserAvatar.type === 'emoji' ? currentUserAvatar.emoji : defaultUserAvatar.emoji, ); const [pendingAvatarColor, setPendingAvatarColor] = React.useState( currentUserAvatar.type === 'emoji' ? currentUserAvatar.color : defaultUserAvatar.color, ); const pendingEmojiAvatar: ClientEmojiAvatar = React.useMemo( () => ({ type: 'emoji', emoji: pendingAvatarEmoji, color: pendingAvatarColor, }), [pendingAvatarColor, pendingAvatarEmoji], ); const onEmojiSelect = React.useCallback(selection => { setUpdateAvatarStatus(); setPendingAvatarEmoji(selection.native); }, []); const onColorSelection = React.useCallback((hex: string) => { setUpdateAvatarStatus(); setPendingAvatarColor(hex); }, []); const onSaveAvatar = React.useCallback(async () => { try { await baseSetUserAvatar(pendingEmojiAvatar); setUpdateAvatarStatus('success'); } catch { setUpdateAvatarStatus('failure'); } }, [pendingEmojiAvatar, baseSetUserAvatar]); let saveButtonContent; let buttonColor; if (userAvatarSaveInProgress) { buttonColor = buttonThemes.standard; saveButtonContent = ; } else if (updateAvatarStatus === 'success') { buttonColor = buttonThemes.success; saveButtonContent = ( <> {'Avatar update succeeded.'} ); } else if (updateAvatarStatus === 'failure') { buttonColor = buttonThemes.danger; saveButtonContent = ( <> {'Avatar update failed. Please try again.'} ); } else { buttonColor = buttonThemes.standard; saveButtonContent = 'Save Avatar'; } const [currentTabType, setCurrentTabType] = React.useState('emoji'); return (
); } export default EmojiAvatarSelectionModal;