diff --git a/assets/images/avatars/user/custom-avatars/season-f1/car-blue100.svg b/assets/images/avatars/user/custom-avatars/season-f1/car-blue100.svg index 2bccc8f87fb6..d1fdf1932f04 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/car-blue100.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/car-blue100.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/car-green100.svg b/assets/images/avatars/user/custom-avatars/season-f1/car-green100.svg index 5c0c58c02339..6b01de919f6c 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/car-green100.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/car-green100.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/car-ice100.svg b/assets/images/avatars/user/custom-avatars/season-f1/car-ice100.svg index a096c25bdaa8..25fff84822fd 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/car-ice100.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/car-ice100.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/car-pink100.svg b/assets/images/avatars/user/custom-avatars/season-f1/car-pink100.svg index 8cb002c436ee..995325846baa 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/car-pink100.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/car-pink100.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/car-tangerine100.svg b/assets/images/avatars/user/custom-avatars/season-f1/car-tangerine100.svg index ca3e8d4126d9..9b3195d662c9 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/car-tangerine100.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/car-tangerine100.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/car-yellow100.svg b/assets/images/avatars/user/custom-avatars/season-f1/car-yellow100.svg index 68238f454517..61c3d550325e 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/car-yellow100.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/car-yellow100.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/champagne-green400.svg b/assets/images/avatars/user/custom-avatars/season-f1/champagne-green400.svg index 63c2d988fd6a..ab63f0f4a70c 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/champagne-green400.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/champagne-green400.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/cone-tangerine700.svg b/assets/images/avatars/user/custom-avatars/season-f1/cone-tangerine700.svg index a981bc14414e..ea89e02a0c25 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/cone-tangerine700.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/cone-tangerine700.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/flag-blue600.svg b/assets/images/avatars/user/custom-avatars/season-f1/flag-blue600.svg index 6147d867fc21..6d787f0947a4 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/flag-blue600.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/flag-blue600.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/gasoline-tangerine400.svg b/assets/images/avatars/user/custom-avatars/season-f1/gasoline-tangerine400.svg index 21510d5a4fb6..bc1172e497e7 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/gasoline-tangerine400.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/gasoline-tangerine400.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/helmet-blue400.svg b/assets/images/avatars/user/custom-avatars/season-f1/helmet-blue400.svg index 9b4111e88c92..05a5350198b5 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/helmet-blue400.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/helmet-blue400.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/helmet-green400.svg b/assets/images/avatars/user/custom-avatars/season-f1/helmet-green400.svg index 249b9d1b272e..78727a3ab81a 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/helmet-green400.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/helmet-green400.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/helmet-ice400.svg b/assets/images/avatars/user/custom-avatars/season-f1/helmet-ice400.svg index 6f627a833b08..5953e4527cc6 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/helmet-ice400.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/helmet-ice400.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/helmet-pink400.svg b/assets/images/avatars/user/custom-avatars/season-f1/helmet-pink400.svg index 233fe74a2719..1ffceea3fa3a 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/helmet-pink400.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/helmet-pink400.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/helmet-tangerine400.svg b/assets/images/avatars/user/custom-avatars/season-f1/helmet-tangerine400.svg index 36b6df6dbd6a..8e0028477bf7 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/helmet-tangerine400.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/helmet-tangerine400.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/helmet-yellow400.svg b/assets/images/avatars/user/custom-avatars/season-f1/helmet-yellow400.svg index 8e4fbde94bd7..ec7d8466b0ab 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/helmet-yellow400.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/helmet-yellow400.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/medal-yellow400.svg b/assets/images/avatars/user/custom-avatars/season-f1/medal-yellow400.svg index 62103049b21c..bf32a8369206 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/medal-yellow400.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/medal-yellow400.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/podium-blue400.svg b/assets/images/avatars/user/custom-avatars/season-f1/podium-blue400.svg index 5fce2902e9df..e6c36b47319f 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/podium-blue400.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/podium-blue400.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/speedometer-ice400.svg b/assets/images/avatars/user/custom-avatars/season-f1/speedometer-ice400.svg index d55e12c94e64..f302095f2c8e 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/speedometer-ice400.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/speedometer-ice400.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/steeringwheel-pink400.svg b/assets/images/avatars/user/custom-avatars/season-f1/steeringwheel-pink400.svg index c7e96b8de37b..a8a0462bca23 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/steeringwheel-pink400.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/steeringwheel-pink400.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/stopwatch-ice600.svg b/assets/images/avatars/user/custom-avatars/season-f1/stopwatch-ice600.svg index 401186840f85..1928a77fff6b 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/stopwatch-ice600.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/stopwatch-ice600.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/tire-green400.svg b/assets/images/avatars/user/custom-avatars/season-f1/tire-green400.svg index 9ceaaeee0274..afebe179ea51 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/tire-green400.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/tire-green400.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/trophy-yellow600.svg b/assets/images/avatars/user/custom-avatars/season-f1/trophy-yellow600.svg index 5c0a292e3676..d04e9ed889b8 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/trophy-yellow600.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/trophy-yellow600.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/images/avatars/user/custom-avatars/season-f1/wrenches-pink600.svg b/assets/images/avatars/user/custom-avatars/season-f1/wrenches-pink600.svg index 85b82eda71b7..f80679fbfdca 100644 --- a/assets/images/avatars/user/custom-avatars/season-f1/wrenches-pink600.svg +++ b/assets/images/avatars/user/custom-avatars/season-f1/wrenches-pink600.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index bdaeadd70ed9..68097f74bcee 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -41,9 +41,6 @@ type AvatarCropModalProps = { /** Callback to be called when user closes the modal */ onClose?: () => void; - /** Callback to be called when user presses the back button */ - onBackButtonPress?: () => void; - /** Callback to be called when user saves the image */ onSave?: (newImage: File | CustomRNImageManipulatorResult) => void; @@ -58,7 +55,7 @@ type AvatarCropModalProps = { }; // This component can't be written using class since reanimated API uses hooks. -function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose, onSave, onBackButtonPress, isVisible, maskImage, buttonLabel}: AvatarCropModalProps) { +function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose, onSave, isVisible, maskImage, buttonLabel}: AvatarCropModalProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -374,7 +371,7 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose {shouldUseNarrowLayout && } {translate('avatarCropModal.description')} {label} )} + {CUSTOM_AVATAR_CATALOG.map(({id, local}) => { + const isSelected = selectedID === id; + + return ( + onSelect(id)} + style={[styles.avatarSelectorWrapper, isSelected && {borderColor: theme.success, borderWidth: 2}]} + > + + + ); + })} {avatarList.map(({id, StyledLetterAvatar}) => { const isSelected = selectedID === id; diff --git a/src/hooks/useAvatarMenu.ts b/src/hooks/useAvatarMenu.ts index d29162cb3ab9..fa53973385c0 100644 --- a/src/hooks/useAvatarMenu.ts +++ b/src/hooks/useAvatarMenu.ts @@ -46,25 +46,22 @@ function useAvatarMenu({isUsingDefaultAvatar, isAvatarSelected, accountID, onIma value: null, }, ]; - // If current avatar is a default avatar and for no avatar is selected in the form, only show upload option - if (isUsingDefaultAvatar && !isAvatarSelected) { - return menuItems; - } - menuItems.push({ - icon: Expensicons.Trashcan, - text: translate('avatarWithImagePicker.removePhoto'), - value: null, - onSelected: () => { - clearError(); - onImageRemoved(); - }, - }); - // If an avatar is selected in the form do NOT show view photo - if (isAvatarSelected) { + // If current avatar is a default avatar and for avatar is selected in the form, only show upload option + if (isUsingDefaultAvatar || isAvatarSelected) { return menuItems; } + return [ ...menuItems, + { + icon: Expensicons.Trashcan, + text: translate('avatarWithImagePicker.removePhoto'), + value: null, + onSelected: () => { + clearError(); + onImageRemoved(); + }, + }, { value: null, icon: Expensicons.Eye, diff --git a/src/libs/API/parameters/UpdateUserAvatarParams.ts b/src/libs/API/parameters/UpdateUserAvatarParams.ts index 2dce38e8763c..e487f39f0414 100644 --- a/src/libs/API/parameters/UpdateUserAvatarParams.ts +++ b/src/libs/API/parameters/UpdateUserAvatarParams.ts @@ -1,7 +1,8 @@ import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; type UpdateUserAvatarParams = { - file: File | CustomRNImageManipulatorResult; + file?: File | CustomRNImageManipulatorResult; + customExpensifyAvatarID?: string; }; export default UpdateUserAvatarParams; diff --git a/src/libs/Avatars/CustomAvatarCatalog.ts b/src/libs/Avatars/CustomAvatarCatalog.ts index 817baf180deb..bff17cf6e057 100644 --- a/src/libs/Avatars/CustomAvatarCatalog.ts +++ b/src/libs/Avatars/CustomAvatarCatalog.ts @@ -86,7 +86,7 @@ const SEASON_F1: Record = { 'wrenches-pink600': {local: SeasonF1.WrenchesPink600, url: `${CDN_SEASON_F1}/wrenches-pink600.png`}, }; -const LETTER_DEFAULTS: Record = { +const LETTER_DEFAULTS: Record> = { 'letter-default-avatar_0': {local: LetterDefaultAvatars.Workspace0}, 'letter-default-avatar_1': {local: LetterDefaultAvatars.Workspace1}, 'letter-default-avatar_2': {local: LetterDefaultAvatars.Workspace2}, @@ -217,4 +217,13 @@ const CUSTOM_AVATAR_CATALOG = buildOrderedAvatars(); const getAvatarLocal = (id: CustomAvatarID) => ALL_CUSTOM_AVATARS[id].local; const getAvatarURL = (id: CustomAvatarID) => ALL_CUSTOM_AVATARS[id].url; -export {ALL_CUSTOM_AVATARS, CUSTOM_AVATAR_CATALOG, LETTER_AVATAR_COLOR_OPTIONS, LETTER_DEFAULTS, getAvatarLocal, getAvatarURL, getLetterAvatar}; +/** + * Type guard to check if a value is a valid CustomAvatarID + * @param value - The value to check + * @returns True if the value is a valid CustomAvatarID + */ +function isCustomAvatarID(value: unknown): value is CustomAvatarID { + return typeof value === 'string' && value in ALL_CUSTOM_AVATARS; +} + +export {ALL_CUSTOM_AVATARS, CUSTOM_AVATAR_CATALOG, LETTER_AVATAR_COLOR_OPTIONS, LETTER_DEFAULTS, getAvatarLocal, getAvatarURL, getLetterAvatar, isCustomAvatarID}; diff --git a/src/libs/Avatars/CustomAvatarCatalog.types.ts b/src/libs/Avatars/CustomAvatarCatalog.types.ts index b93d5a719ded..ef8571dec2c7 100644 --- a/src/libs/Avatars/CustomAvatarCatalog.types.ts +++ b/src/libs/Avatars/CustomAvatarCatalog.types.ts @@ -91,7 +91,7 @@ type LetterAvatarIDs = | 'letter-default-avatar_z'; type LetterAvatarColorStyle = {backgroundColor: string; fillColor: string}; -type AvatarEntry = {local: React.FC; url?: string}; +type AvatarEntry = {local: React.FC; url: string}; type CustomAvatarID = DefaultAvatarIDs | SeasonF1AvatarIDs; type LetterAvatarVariant = { diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 0b3462d7c6bf..9a52f1d65f13 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -8,6 +8,8 @@ import type {LoginList, PrivatePersonalDetails, VacationDelegate} from '@src/typ import type Login from '@src/types/onyx/Login'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; +import {ALL_CUSTOM_AVATARS, getAvatarLocal} from './Avatars/CustomAvatarCatalog'; +import type {CustomAvatarID} from './Avatars/CustomAvatarCatalog.types'; import hashCode from './hashCode'; import {formatPhoneNumber} from './LocalePhoneNumber'; import {translateLocal} from './Localize'; @@ -133,7 +135,7 @@ function getAccountIDHashBucket(accountID = -1, avatarURL?: string) { /** * Helper method to return the default avatar associated with the given accountID */ -function getDefaultAvatar(accountID = -1, avatarURL?: string): IconAsset | undefined { +function getDefaultAvatar(accountID: number = CONST.DEFAULT_NUMBER_ID, avatarURL?: string): IconAsset | undefined { if (accountID === CONST.ACCOUNT_ID.CONCIERGE) { return ConciergeAvatar; } @@ -149,18 +151,42 @@ function getDefaultAvatar(accountID = -1, avatarURL?: string): IconAsset | undef return defaultAvatars[`Avatar${accountIDHashBucket}`]; } +/** + * Helper method to return default avatar name associated with the accountID + */ +function getDefaultAvatarName(accountID: number = CONST.DEFAULT_NUMBER_ID, avatarURL?: string): string { + const accountIDHashBucket = getAccountIDHashBucket(accountID, avatarURL); + const avatarPrefix = `default-avatar`; + + return `${avatarPrefix}_${accountIDHashBucket}`; +} + /** * Helper method to return default avatar URL associated with the accountID */ -function getDefaultAvatarURL(accountID: string | number = '', avatarURL?: string): string { +function getDefaultAvatarURL(accountID: number = CONST.DEFAULT_NUMBER_ID, avatarURL?: string): string { if (Number(accountID) === CONST.ACCOUNT_ID.CONCIERGE) { return CONST.CONCIERGE_ICON_URL; } - const accountIDHashBucket = getAccountIDHashBucket(Number(accountID) || -1, avatarURL); - const avatarPrefix = `default-avatar`; + return `${CONST.CLOUDFRONT_URL}/images/avatars/${getDefaultAvatarName(accountID, avatarURL)}.png`; +} - return `${CONST.CLOUDFRONT_URL}/images/avatars/${avatarPrefix}_${accountIDHashBucket}.png`; +/** + * Helper method to extract the avatar name from a default avatar URL + * @param avatarURL - the URL returned by getDefaultAvatarURL + * @returns the avatar name (e.g., 'default-avatar_5', 'concierge') or undefined if not a valid default avatar URL + */ +function getDefaultAvatarNameFromURL(avatarURL?: AvatarSource): string | undefined { + if (!avatarURL || typeof avatarURL !== 'string' || avatarURL === CONST.CONCIERGE_ICON_URL) { + return undefined; + } + + // Extract avatar name from CloudFront URL and make sure it's one of defaults + const match = avatarURL.split('/').at(-1)?.split('.')?.[0] ?? ''; + if (ALL_CUSTOM_AVATARS[match as CustomAvatarID]) { + return match; + } } /** @@ -225,6 +251,10 @@ function getSmallSizeAvatar(avatarSource?: AvatarSource, accountID?: number): Av if (typeof source !== 'string') { return source; } + const maybeDefaultAvatarName = getDefaultAvatarNameFromURL(avatarSource); + if (maybeDefaultAvatarName) { + return getAvatarLocal(maybeDefaultAvatarName as CustomAvatarID); + } // Because other urls than CloudFront do not support dynamic image sizing (_SIZE suffix), the current source is already what we want to use here. if (!CONST.CLOUDFRONT_DOMAIN_REGEX.test(source)) { diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 1556f0060d5e..9751e46a28c1 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -357,10 +357,22 @@ function openPublicProfilePage(accountID: number) { API.read(READ_COMMANDS.OPEN_PUBLIC_PROFILE_PAGE, parameters, {optimisticData, successData, failureData}); } +type DefaultAvatarResult = {uri: string; name: string; customExpensifyAvatarID: string}; + +/** + * Type guard to check if a file object is a DefaultAvatarResult + */ +function isDefaultAvatarResult(file: File | CustomRNImageManipulatorResult | DefaultAvatarResult): file is DefaultAvatarResult { + return 'customExpensifyAvatarID' in file && typeof file.customExpensifyAvatarID === 'string'; +} + /** * Updates the user's avatar image */ -function updateAvatar(file: File | CustomRNImageManipulatorResult, currentUserPersonalDetails: Pick) { +function updateAvatar( + file: File | CustomRNImageManipulatorResult | DefaultAvatarResult, + currentUserPersonalDetails: Pick, +) { if (!currentUserPersonalDetails.accountID) { return; } @@ -415,7 +427,7 @@ function updateAvatar(file: File | CustomRNImageManipulatorResult, currentUserPe }, ]; - const parameters: UpdateUserAvatarParams = {file}; + const parameters: UpdateUserAvatarParams = isDefaultAvatarResult(file) ? {customExpensifyAvatarID: file.customExpensifyAvatarID} : {file}; API.write(WRITE_COMMANDS.UPDATE_USER_AVATAR, parameters, {optimisticData, successData, failureData}); } diff --git a/src/pages/settings/Profile/Avatar/AvatarPage.tsx b/src/pages/settings/Profile/Avatar/AvatarPage.tsx index efa9f2d3c711..7ff7798d8745 100644 --- a/src/pages/settings/Profile/Avatar/AvatarPage.tsx +++ b/src/pages/settings/Profile/Avatar/AvatarPage.tsx @@ -17,9 +17,11 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useLetterAvatars from '@hooks/useLetterAvatars'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import {getAvatarLocal, getAvatarURL, isCustomAvatarID} from '@libs/Avatars/CustomAvatarCatalog'; import {validateAvatarImage} from '@libs/AvatarUtils'; import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import Navigation from '@libs/Navigation/Navigation'; +import type {AvatarSource} from '@libs/UserUtils'; import {isDefaultAvatar} from '@libs/UserUtils'; import DiscardChangesConfirmation from '@pages/iou/request/step/DiscardChangesConfirmation'; import {deleteAvatar, updateAvatar} from '@userActions/PersonalDetails'; @@ -29,8 +31,6 @@ import type {FileObject} from '@src/types/utils/Attachment'; import AvatarCapture from './AvatarCapture'; import type {AvatarCaptureHandle} from './AvatarCapture/types'; -const EMPTY_FILE = {uri: '', name: '', type: '', file: null}; - type ImageData = { uri: string; name: string; @@ -43,16 +43,23 @@ type ErrorData = { phraseParam: Record; }; +const EMPTY_FILE = {uri: '', name: '', type: '', file: null}; + function ProfileAvatar() { const [errorData, setErrorData] = useState({validationError: null, phraseParam: {}}); const [isAvatarCropModalOpen, setIsAvatarCropModalOpen] = useState(false); const [selected, setSelected] = useState(); const avatarCaptureRef = useRef(null); + const isSavingRef = useRef(false); const styles = useThemeStyles(); const {translate} = useLocalize(); + const [cropImageData, setCropImageData] = useState({...EMPTY_FILE}); const [imageData, setImageData] = useState({...EMPTY_FILE}); + + const isDirty = imageData.uri !== '' || !!selected; + const avatarStyle = [styles.avatarXLarge, styles.alignSelfStart, styles.alignSelfCenter]; const currentUserPersonalDetails = useCurrentUserPersonalDetails(); @@ -60,9 +67,17 @@ function ProfileAvatar() { const accountID = currentUserPersonalDetails?.accountID ?? CONST.DEFAULT_NUMBER_ID; // eslint-disable-next-line no-nested-ternary - const avatarURL = selected ? avatars[selected] : imageData.uri !== '' ? imageData.uri : (currentUserPersonalDetails?.avatar ?? ''); + let avatarURL: AvatarSource = ''; + if (selected && isCustomAvatarID(selected)) { + avatarURL = getAvatarLocal(selected); + } else if (selected) { + avatarURL = avatars[selected]; + } else if (imageData.uri) { + avatarURL = imageData.uri; + } else { + avatarURL = currentUserPersonalDetails?.avatar ?? ''; + } const isUsingDefaultAvatar = isDefaultAvatar(currentUserPersonalDetails?.avatar ?? ''); - const isDirty = !!imageData.uri || !!selected; const setError = (error: TranslationPaths | null, phraseParam: Record) => { setErrorData({ @@ -84,7 +99,7 @@ function ProfileAvatar() { setIsAvatarCropModalOpen(true); setError(null, {}); - setImageData({ + setCropImageData({ uri: image.uri ?? '', name: image.name ?? '', type: image.type ?? '', @@ -99,8 +114,8 @@ function ProfileAvatar() { const onImageSelected = useCallback((file: File | CustomRNImageManipulatorResult) => { setSelected(undefined); setImageData({ - uri: file.uri ?? '', - name: file.name, + uri: file?.uri ?? '', + name: file?.name, file, type: '', }); @@ -137,6 +152,8 @@ function ProfileAvatar() { }); const onPress = useCallback(() => { + isSavingRef.current = true; + if (imageData.file) { updateAvatar(imageData.file, { avatar: currentUserPersonalDetails?.avatar, @@ -145,25 +162,43 @@ function ProfileAvatar() { }); setImageData({...EMPTY_FILE}); Navigation.dismissModal(); + isSavingRef.current = false; + return; + } + + if (selected && isCustomAvatarID(selected)) { + updateAvatar( + { + uri: getAvatarURL(selected), + name: selected, + customExpensifyAvatarID: selected, + }, + { + avatar: currentUserPersonalDetails?.avatar, + avatarThumbnail: currentUserPersonalDetails?.avatarThumbnail, + accountID: currentUserPersonalDetails?.accountID, + }, + ); + setSelected(undefined); + Navigation.dismissModal(); + isSavingRef.current = false; return; } if (!selected || !avatarCaptureRef.current) { + isSavingRef.current = false; return; } // User selected a letter avatar - avatarCaptureRef.current - .capture() - ?.then((file) => { - updateAvatar(file, { - avatar: currentUserPersonalDetails?.avatar, - avatarThumbnail: currentUserPersonalDetails?.avatarThumbnail, - accountID: currentUserPersonalDetails?.accountID, - }); - setSelected(undefined); - }) - .then(() => { - Navigation.dismissModal(); + avatarCaptureRef.current.capture()?.then((file) => { + updateAvatar(file, { + avatar: currentUserPersonalDetails?.avatar, + avatarThumbnail: currentUserPersonalDetails?.avatarThumbnail, + accountID: currentUserPersonalDetails?.accountID, }); + setSelected(undefined); + Navigation.dismissModal(); + isSavingRef.current = false; + }); }, [currentUserPersonalDetails?.accountID, currentUserPersonalDetails?.avatar, currentUserPersonalDetails?.avatarThumbnail, imageData.file, selected]); return ( @@ -235,7 +270,7 @@ function ProfileAvatar() { contentContainerStyle={styles.flexGrow1} keyboardShouldPersistTaps="handled" > - + - +