Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions src/components/Avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {getAvatarLocal} from '@libs/Avatars/CustomAvatarCatalog';
import {getDefaultWorkspaceAvatar, getDefaultWorkspaceAvatarTestID} from '@libs/ReportUtils';
import type {AvatarSource} from '@libs/UserUtils';
import {getAvatar, getDefaultAvatarNameFromURL} from '@libs/UserUtils';
import {getAvatar} from '@libs/UserUtils';
import type {AvatarSizeName} from '@styles/utils';
import CONST from '@src/CONST';
import type {AvatarType} from '@src/types/onyx/OnyxCommon';
Expand Down Expand Up @@ -88,16 +87,10 @@ function Avatar({
const userAccountID = isWorkspace ? undefined : (avatarID as number);

const source = isWorkspace ? originalSource : getAvatar(originalSource, userAccountID);
let optimizedSource = source;
const maybeDefaultAvatarName = getDefaultAvatarNameFromURL(source);

if (maybeDefaultAvatarName) {
optimizedSource = getAvatarLocal(maybeDefaultAvatarName);
}
const useFallBackAvatar = imageError || !source || source === Expensicons.FallbackAvatar;
const fallbackAvatar = isWorkspace ? getDefaultWorkspaceAvatar(name) : fallbackIcon || Expensicons.FallbackAvatar;
const fallbackAvatarTestID = isWorkspace ? getDefaultWorkspaceAvatarTestID(name) : fallbackIconTestID || 'SvgFallbackAvatar Icon';
const avatarSource = useFallBackAvatar ? fallbackAvatar : optimizedSource;
const avatarSource = useFallBackAvatar ? fallbackAvatar : source;

// We pass the color styles down to the SVG for the workspace and fallback avatar.
const iconSize = StyleUtils.getAvatarSize(size);
Expand Down
33 changes: 12 additions & 21 deletions src/hooks/useAvatarMenu.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {useCallback, useContext} from 'react';
import {useCallback} from 'react';
import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types';
import * as Expensicons from '@components/Icon/Expensicons';
import Navigation from '@libs/Navigation/Navigation';
import AttachmentModalContext from '@pages/media/AttachmentModalScreen/AttachmentModalContext';
import ROUTES from '@src/ROUTES';
import type {FileObject} from '@src/types/utils/Attachment';
import useLocalize from './useLocalize';
Expand All @@ -12,10 +11,8 @@ type OpenPicker = (options: {onPicked: (files: FileObject[]) => void}) => void;
type UseAvatarMenuParams = {
/** Whether the user is using a default avatar */
isUsingDefaultAvatar: boolean;
/** Source of newly uploaded avatar */
source?: string;
/** File name of newly uploaded avatar */
originalFileName?: string;
/** Whether the user has chosen a new avatar in the form but hasn't uploaded it yet */
isAvatarSelected: boolean;
/** Account ID for navigation */
accountID: number;
/** Callback when avatar is removed */
Expand All @@ -29,9 +26,8 @@ type UseAvatarMenuParams = {
/**
* Custom hook to create avatar menu items
*/
function useAvatarMenu({isUsingDefaultAvatar, accountID, onImageRemoved, showAvatarCropModal, clearError, source, originalFileName}: UseAvatarMenuParams) {
function useAvatarMenu({isUsingDefaultAvatar, isAvatarSelected, accountID, onImageRemoved, showAvatarCropModal, clearError}: UseAvatarMenuParams) {
const {translate} = useLocalize();
const attachmentContext = useContext(AttachmentModalContext);

/**
* Create menu items list for avatar menu
Expand All @@ -51,35 +47,30 @@ function useAvatarMenu({isUsingDefaultAvatar, accountID, onImageRemoved, showAva
},
];
// If current avatar is a default avatar and for avatar is selected in the form, only show upload option
if (isUsingDefaultAvatar) {
if (isUsingDefaultAvatar || isAvatarSelected) {
return menuItems;
}
if (!source) {
menuItems.push({

return [
...menuItems,
{
icon: Expensicons.Trashcan,
text: translate('avatarWithImagePicker.removePhoto'),
value: null,
onSelected: () => {
clearError();
onImageRemoved();
},
});
}

return [
...menuItems,
},
{
value: null,
icon: Expensicons.Eye,
text: translate('avatarWithImagePicker.viewPhoto'),
onSelected: () => {
attachmentContext.setCurrentAttachment({source, originalFileName});
Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(accountID));
},
onSelected: () => Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(accountID)),
},
];
},
[translate, isUsingDefaultAvatar, source, showAvatarCropModal, clearError, onImageRemoved, attachmentContext, originalFileName, accountID],
[accountID, isUsingDefaultAvatar, onImageRemoved, showAvatarCropModal, translate, clearError, isAvatarSelected],
);

return {createMenuItems};
Expand Down
2 changes: 0 additions & 2 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2467,8 +2467,6 @@ type AttachmentModalScreensParamList = {
};
[SCREENS.PROFILE_AVATAR]: AttachmentModalContainerModalProps & {
accountID: number;
source?: AvatarSource;
originalFileName?: string;
// eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md
backTo?: Routes;
};
Expand Down
30 changes: 4 additions & 26 deletions src/libs/UserUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,14 +180,14 @@ function getDefaultAvatarURL(accountID: number = CONST.DEFAULT_NUMBER_ID, accoun
* @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): CustomAvatarID | undefined {
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] ?? '') as CustomAvatarID;
if (ALL_CUSTOM_AVATARS[match]) {
const match = avatarURL.split('/').at(-1)?.split('.')?.[0] ?? '';
if (ALL_CUSTOM_AVATARS[match as CustomAvatarID]) {
return match;
}
}
Expand All @@ -211,25 +211,6 @@ function isDefaultAvatar(avatarSource?: AvatarSource): avatarSource is string |
return false;
}

/**
* * Given a user's avatar path and originalFileName, returns true if URL points to a default avatar, false otherwise
* @param avatarSource - the avatar source from user's personalDetails
* @param originalFileName - the avatar original file name from user's personalDetails
*/
function isDefaultOrCustomDefaultAvatar(avatarSource?: AvatarSource, originalFileName?: string): boolean {
if (
(typeof avatarSource === 'string' && avatarSource.includes('images/avatars/custom-avatars')) || // F1 avatars
(originalFileName && /^letter-avatar-#[0-9A-F]{6}-#[0-9A-F]{6}-[A-Z]\.png$/.test(originalFileName)) // Letter avatars
) {
return true;
}
if (isDefaultAvatar(avatarSource)) {
return true;
}

return false;
}

/**
* Provided an avatar source, if source is a default avatar, return the associated SVG.
* Otherwise, return the URL or SVG pointing to the user-uploaded avatar.
Expand Down Expand Up @@ -276,7 +257,7 @@ function getSmallSizeAvatar(avatarSource?: AvatarSource, accountID?: number, acc
}
const maybeDefaultAvatarName = getDefaultAvatarNameFromURL(avatarSource);
if (maybeDefaultAvatarName) {
return getAvatarLocal(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.
Expand Down Expand Up @@ -363,8 +344,6 @@ export {
generateAccountID,
getAvatar,
getAvatarUrl,
getDefaultAvatarName,
getDefaultAvatarNameFromURL,
getDefaultAvatarURL,
getFullSizeAvatar,
getLoginListBrickRoadIndicator,
Expand All @@ -374,7 +353,6 @@ export {
hasLoginListError,
hasLoginListInfo,
hashText,
isDefaultOrCustomDefaultAvatar,
isDefaultAvatar,
getContactMethod,
isCurrentUserValidated,
Expand Down
1 change: 0 additions & 1 deletion src/libs/actions/PersonalDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,6 @@ function updateAvatar(
API.write(WRITE_COMMANDS.UPDATE_USER_AVATAR, parameters, {optimisticData, successData, failureData});
}

// TODO remove when no longer needed
/**
* Replaces the user's avatar image with a default avatar
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type SCREENS from '@src/SCREENS';
import useDownloadAttachment from './hooks/useDownloadAttachment';

function ProfileAvatarModalContent({navigation, route}: AttachmentModalScreenProps<typeof SCREENS.PROFILE_AVATAR>) {
const {accountID = CONST.DEFAULT_NUMBER_ID, source: tempSource, originalFileName: tempOriginalFileName} = route.params;
const {accountID = CONST.DEFAULT_NUMBER_ID} = route.params;

const {formatPhoneNumber} = useLocalize();

Expand All @@ -34,8 +34,8 @@ function ProfileAvatarModalContent({navigation, route}: AttachmentModalScreenPro
openPublicProfilePage(accountID);
}, [accountID]);

const source = tempSource ?? getFullSizeAvatar(avatarURL, accountID);
const originalFileName = tempOriginalFileName ?? personalDetail?.originalFileName ?? '';
const source = getFullSizeAvatar(avatarURL, accountID);
const originalFileName = personalDetail?.originalFileName ?? '';
const headerTitle = formatPhoneNumber(displayName);
// eslint-disable-next-line rulesdir/no-negated-variables
const shouldShowNotFoundPage = !avatarURL;
Expand Down
43 changes: 26 additions & 17 deletions src/pages/settings/Profile/Avatar/AvatarPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ 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 {getDefaultAvatarName, isDefaultOrCustomDefaultAvatar} from '@libs/UserUtils';
import {isDefaultAvatar} from '@libs/UserUtils';
import DiscardChangesConfirmation from '@pages/iou/request/step/DiscardChangesConfirmation';
import {updateAvatar} from '@userActions/PersonalDetails';
import {deleteAvatar, updateAvatar} from '@userActions/PersonalDetails';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type {FileObject} from '@src/types/utils/Attachment';
Expand Down Expand Up @@ -77,8 +77,7 @@ function ProfileAvatar() {
} else {
avatarURL = currentUserPersonalDetails?.avatar ?? '';
}

const isUsingDefaultAvatar = (!imageData.uri && isDefaultOrCustomDefaultAvatar(currentUserPersonalDetails?.avatar, currentUserPersonalDetails?.originalFileName)) || !!selected;
const isUsingDefaultAvatar = isDefaultAvatar(currentUserPersonalDetails?.avatar ?? '');

const setError = (error: TranslationPaths | null, phraseParam: Record<string, unknown>) => {
setErrorData({
Expand Down Expand Up @@ -124,22 +123,33 @@ function ProfileAvatar() {
}, []);

const onImageRemoved = useCallback(() => {
setSelected(getDefaultAvatarName(currentUserPersonalDetails?.accountID, currentUserPersonalDetails?.email));
if (isDirty) {
setSelected(undefined);
setImageData({...EMPTY_FILE});
return;
}
deleteAvatar({
avatar: currentUserPersonalDetails?.avatar,
fallbackIcon: currentUserPersonalDetails?.fallbackIcon,
accountID: currentUserPersonalDetails?.accountID,
email: currentUserPersonalDetails?.email,
});
setSelected(undefined);
setImageData({...EMPTY_FILE});
}, [currentUserPersonalDetails?.accountID, currentUserPersonalDetails?.email]);
Navigation.dismissModal();
}, [currentUserPersonalDetails, isDirty]);

const clearError = useCallback(() => {
setError(null, {});
}, []);

const {createMenuItems} = useAvatarMenu({
isAvatarSelected: isDirty,
isUsingDefaultAvatar,
accountID,
onImageRemoved,
showAvatarCropModal,
clearError,
source: imageData.uri,
originalFileName: imageData.name,
});

const onPress = useCallback(() => {
Expand Down Expand Up @@ -187,7 +197,6 @@ function ProfileAvatar() {
accountID: currentUserPersonalDetails?.accountID,
});
setSelected(undefined);
setImageData({...EMPTY_FILE});
Navigation.dismissModal();
isSavingRef.current = false;
});
Expand Down Expand Up @@ -272,17 +281,17 @@ function ProfileAvatar() {
setSelected(id);
}}
/>
{!!errorData.validationError && (
<DotIndicatorMessage
style={styles.mt6}
// eslint-disable-next-line @typescript-eslint/naming-convention
messages={{0: translate(errorData.validationError, errorData.phraseParam as never)}}
type="error"
/>
)}
</View>
</ScrollView>
<FixedFooter style={styles.mtAuto}>
{!!errorData.validationError && (
<DotIndicatorMessage
style={styles.mv5}
// eslint-disable-next-line @typescript-eslint/naming-convention
messages={{0: translate(errorData.validationError, errorData.phraseParam as never)}}
type="error"
/>
)}
<Button
large
success
Expand Down
Loading