Skip to content
7 changes: 3 additions & 4 deletions src/components/Avatar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {AvatarSource} from '@libs/UserAvatarUtils';
import {getAccountIDFromAvatarID} from '@libs/UserAvatarUtils';

import CONST from '@src/CONST';
import type {AvatarType} from '@src/types/onyx/OnyxCommon';
Expand Down Expand Up @@ -28,7 +29,7 @@ type AvatarProps = AvatarCommonProps & {
};

/** Renders a user or workspace avatar depending on the `type` prop.
* This wrapper exists for backward compatibility. If possible use WorkspaceAvatar and UserAvatar directly.
* Prefer `WorkspaceAvatar` and `UserAvatar` directly when the avatar kind is known at the call site.
*/
function Avatar({
type,
Expand Down Expand Up @@ -60,8 +61,6 @@ function Avatar({
);
}

const accountID = typeof avatarID === 'string' ? parseInt(avatarID, 10) : avatarID;

return (
<UserAvatar
fallbackIcon={fallbackIcon}
Expand All @@ -73,7 +72,7 @@ function Avatar({
size={size}
fill={fill}
testID={testID}
accountID={accountID}
accountID={getAccountIDFromAvatarID(avatarID)}
/>
);
}
Expand Down
59 changes: 11 additions & 48 deletions src/components/AvatarButtonWithIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';

import type {AvatarSource} from '@libs/UserAvatarUtils';

import variables from '@styles/variables';

import CONST from '@src/CONST';
Expand All @@ -14,11 +12,11 @@ import type WithSentryLabel from '@src/types/utils/SentryLabel';

import type {RefObject} from 'react';
import type {ImageStyle, StyleProp, ViewStyle} from 'react-native';
import type {ValueOf} from 'type-fest';

import React from 'react';
import {StyleSheet, View} from 'react-native';

import Avatar from './Avatar';
import Icon from './Icon';
import OfflineWithFeedback from './OfflineWithFeedback';
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
Expand All @@ -28,7 +26,7 @@ type AvatarButtonWithIconProps = WithSentryLabel & {
/** Text to be used as a tooltip */
text: string;

/** Style applied to the avatar */
/** Style applied to the avatar. Defaults to centering it within the parent. */
avatarStyle?: StyleProp<ViewStyle & ImageStyle>;

/** Executed on click */
Expand All @@ -37,30 +35,18 @@ type AvatarButtonWithIconProps = WithSentryLabel & {
/** Ref of the anchor */
anchorRef?: RefObject<View | HTMLDivElement | null>;

/** Account id of user for which avatar is displayed */
avatarID?: number | string;
/** The avatar to display. */
avatar: React.ReactNode;

/** Avatar source to display */
source?: AvatarSource;
/** Size of the displayed avatar. Keeps the button footprint even when `avatar` is empty. */
size?: ValueOf<typeof CONST.AVATAR_SIZE>;

/** Additional style props for disabled picker */
disabledStyle?: StyleProp<ViewStyle>;

/** Additional style props for the edit icon */
editIconStyle?: StyleProp<ViewStyle>;

/** A default avatar component to display when there is no source */
DefaultAvatar?: () => React.ReactNode;

/** Size of Indicator */
size?: typeof CONST.AVATAR_SIZE.XXXX_LARGE | typeof CONST.AVATAR_SIZE.XXX_LARGE | typeof CONST.AVATAR_SIZE.DEFAULT;

/** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */
fallbackIcon?: AvatarSource;

/** Denotes whether it is an avatar or a workspace avatar */
type?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE;

/** The type of action that's pending */
pendingAction?: OnyxCommon.PendingAction;

Expand All @@ -69,37 +55,29 @@ type AvatarButtonWithIconProps = WithSentryLabel & {

/** Optionally override the default "Edit" icon */
editIcon?: IconAsset;

/** The name associated with avatar */
name?: string;
};

/**
* Avatar button with an edit icon overlay
*/
function AvatarButtonWithIcon({
DefaultAvatar = () => null,
disabledStyle,
editIconStyle,
pendingAction,
text,
onPress,
source = '',
avatarID,
fallbackIcon,
size = CONST.AVATAR_SIZE.DEFAULT,
type = CONST.ICON_TYPE_AVATAR,
avatar,
size = CONST.AVATAR_SIZE.XXXX_LARGE,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we correct to change the default size from CONST.AVATAR_SIZE.DEFAULT to CONST.AVATAR_SIZE.XXXX_LARGE?

@jmusial jmusial Aug 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On main, old DEFAULT default was dead code - every call site passed size={CONST.AVATAR_SIZE.XXXX_LARGE}

avatarStyle,
disabled = false,
editIcon,
anchorRef,
name = '',
sentryLabel,
}: AvatarButtonWithIconProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['FallbackAvatar', 'Pencil']);
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Pencil']);

return (
<Tooltip
Expand All @@ -112,26 +90,11 @@ function AvatarButtonWithIcon({
accessibilityLabel={text}
disabled={disabled}
disabledStyle={disabledStyle}
style={[styles.pRelative, type === CONST.ICON_TYPE_AVATAR && styles.alignSelfCenter, StyleUtils.getWidthAndHeightStyle(StyleUtils.getAvatarSize(size)), avatarStyle]}
style={[styles.pRelative, StyleUtils.getWidthAndHeightStyle(StyleUtils.getAvatarSize(size)), styles.alignSelfCenter, avatarStyle]}
ref={anchorRef}
sentryLabel={sentryLabel}
>
<OfflineWithFeedback pendingAction={pendingAction}>
{source ? (
<Avatar
containerStyles={avatarStyle}
imageStyles={[styles.alignSelfCenter, avatarStyle]}
source={source}
avatarID={avatarID}
fallbackIcon={fallbackIcon ?? expensifyIcons.FallbackAvatar}
size={size}
type={type}
name={name}
/>
) : (
<DefaultAvatar />
)}
</OfflineWithFeedback>
<OfflineWithFeedback pendingAction={pendingAction}>{avatar}</OfflineWithFeedback>
{!disabled && (
<View style={StyleSheet.flatten([styles.smallEditIcon, styles.smallAvatarEditIcon, editIconStyle])}>
<Icon
Expand Down
28 changes: 10 additions & 18 deletions src/components/AvatarWithImagePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {validateAvatarImage} from '@libs/AvatarUtils';
import {isSafari} from '@libs/Browser';
import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types';
import type {AvatarSource} from '@libs/UserAvatarUtils';

import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
Expand Down Expand Up @@ -49,6 +50,9 @@ type AvatarWithImagePickerProps = Omit<AvatarButtonWithIconProps, 'text' | 'onPr
/** Additional style props */
style?: StyleProp<ViewStyle>;

/** Current avatar source. Used to clear the validation error whenever the avatar changes. */
source?: AvatarSource;

/** Executed once an image has been selected */
onImageSelected?: (file: File | CustomRNImageManipulatorResult) => void;

Expand All @@ -75,15 +79,11 @@ type AvatarWithImagePickerProps = Omit<AvatarButtonWithIconProps, 'text' | 'onPr

/** Allows to open an image without Attachment Picker. */
enablePreview?: boolean;

/** The name associated with avatar */
name?: string;
};

const anchorAlignment = {horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP};

function AvatarWithImagePicker({
DefaultAvatar = () => null,
style,
disabledStyle,
editIconStyle,
Expand All @@ -92,10 +92,8 @@ function AvatarWithImagePicker({
errorRowStyles,
onErrorClose = () => {},
source = '',
avatarID,
fallbackIcon,
size = CONST.AVATAR_SIZE.DEFAULT,
type = CONST.ICON_TYPE_AVATAR,
avatar,
size,
isUsingDefaultAvatar = false,
onImageSelected = () => {},
onImageRemoved = () => {},
Expand All @@ -105,10 +103,9 @@ function AvatarWithImagePicker({
onViewPhotoPress,
enablePreview = false,
editIcon,
name = '',
sentryLabel,
}: AvatarWithImagePickerProps) {
const icons = useMemoizedLazyExpensifyIcons(['Eye', 'FallbackAvatar', 'Pencil', 'Trashcan', 'Upload']);
const icons = useMemoizedLazyExpensifyIcons(['Eye', 'Pencil', 'Trashcan', 'Upload']);
const styles = useThemeStyles();
const isFocused = useIsFocused();
const [popoverPosition, setPopoverPosition] = useState({horizontal: 0, vertical: 0});
Expand Down Expand Up @@ -137,7 +134,7 @@ function AvatarWithImagePicker({

useEffect(() => {
setError(null, {});
}, [source, avatarID]);
}, [source]);

/**
* Validates an image and opens avatar crop modal if valid
Expand Down Expand Up @@ -246,21 +243,16 @@ function AvatarWithImagePicker({
>
<AvatarButtonWithIcon
text={translate('avatarWithImagePicker.editImage')}
source={source}
avatarID={avatarID}
avatar={avatar}
size={size}
onPress={() => onPressAvatar(openPicker)}
avatarStyle={avatarStyle}
pendingAction={pendingAction}
fallbackIcon={fallbackIcon ?? icons.FallbackAvatar}
anchorRef={anchorRef}
DefaultAvatar={DefaultAvatar}
editIcon={editIcon ?? icons.Pencil}
size={size}
type={type}
disabled={disabled}
disabledStyle={disabledStyle}
editIconStyle={editIconStyle}
name={name}
sentryLabel={sentryLabel}
/>
</OfflineWithFeedback>
Expand Down
28 changes: 18 additions & 10 deletions src/components/HeaderWithBackButton/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import ActivityIndicator from '@components/ActivityIndicator';
import Avatar from '@components/Avatar';
import UserAvatar from '@components/Avatar/UserAvatar';
import WorkspaceAvatar from '@components/Avatar/WorkspaceAvatar';
import AvatarWithDisplayName from '@components/AvatarWithDisplayName';
import Header from '@components/Header';
import Icon from '@components/Icon';
Expand All @@ -23,6 +24,7 @@ import useThrottledButtonState from '@hooks/useThrottledButtonState';
import getButtonState from '@libs/getButtonState';
import Navigation from '@libs/Navigation/Navigation';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
import {getAccountIDFromAvatarID} from '@libs/UserAvatarUtils';

import variables from '@styles/variables';

Expand Down Expand Up @@ -287,15 +289,21 @@ function HeaderWithBackButton({
fill={iconFill}
/>
)}
{!!policyAvatar && (
<Avatar
containerStyles={styles.mr3}
source={policyAvatar?.source}
name={policyAvatar?.name}
avatarID={policyAvatar?.id}
type={policyAvatar?.type}
/>
)}
{!!policyAvatar &&
(policyAvatar.type === CONST.ICON_TYPE_WORKSPACE ? (
<WorkspaceAvatar
containerStyles={[StyleUtils.getWidthAndHeightStyle(StyleUtils.getAvatarSize(CONST.AVATAR_SIZE.DEFAULT)), styles.mr3]}
source={policyAvatar.source}
name={policyAvatar.name ?? ''}
avatarID={policyAvatar.id ?? CONST.DEFAULT_NUMBER_ID}
/>
) : (
<UserAvatar
containerStyles={[StyleUtils.getWidthAndHeightStyle(StyleUtils.getAvatarSize(CONST.AVATAR_SIZE.DEFAULT)), styles.mr3]}
source={policyAvatar.source}
accountID={getAccountIDFromAvatarID(policyAvatar.id)}
/>
))}
{middleContent}
<View style={[styles.reportOptions, styles.flexRow, styles.alignItemsCenter]}>
<View style={[styles.pr2, styles.flexRow, styles.alignItemsCenter]}>
Expand Down
18 changes: 8 additions & 10 deletions src/components/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import type {PressableRef} from './Pressable/GenericPressable/types';

import ActivityIndicator from './ActivityIndicator';
import Avatar from './Avatar';
import WorkspaceAvatar from './Avatar/WorkspaceAvatar';
import Badge from './Badge';
import {useIsCompactMenu} from './CompactMenuContext';
import CopyTextToClipboard from './CopyTextToClipboard';
Expand Down Expand Up @@ -948,7 +949,7 @@ function MenuItem({
accountIDs={iconAccountID ? [iconAccountID] : undefined}
/>
)}
{!icon && shouldPutLeftPaddingWhenNoIcon && (
{!icon && iconType !== CONST.ICON_TYPE_WORKSPACE && shouldPutLeftPaddingWhenNoIcon && (
<View
style={[
styles.popoverMenuIcon,
Expand All @@ -958,7 +959,7 @@ function MenuItem({
]}
/>
)}
{!!icon && !Array.isArray(icon) && (
{(!!icon || iconType === CONST.ICON_TYPE_WORKSPACE) && !Array.isArray(icon) && (
<View
style={[
styles.popoverMenuIcon,
Expand Down Expand Up @@ -998,15 +999,12 @@ function MenuItem({
reasonAttributes={menuItemLoadingReasonAttributes}
/>
))}
{!!icon && iconType === CONST.ICON_TYPE_WORKSPACE && (
<Avatar
imageStyles={[styles.alignSelfCenter]}
size={CONST.AVATAR_SIZE.DEFAULT}
{iconType === CONST.ICON_TYPE_WORKSPACE && (
<WorkspaceAvatar
imageStyles={styles.alignSelfCenter}
source={icon}
Comment thread
jmusial marked this conversation as resolved.
fallbackIcon={fallbackIcon ?? icons.FallbackAvatar}
name={title}
avatarID={avatarID}
type={CONST.ICON_TYPE_WORKSPACE}
name={title ?? ''}
avatarID={avatarID ?? CONST.DEFAULT_NUMBER_ID}
/>
)}
{iconType === CONST.ICON_TYPE_AVATAR && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute';
import Navigation from '@libs/Navigation/Navigation';
import {canSendInvoice} from '@libs/PolicyUtils';
import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -64,7 +63,7 @@ function InvoiceSenderField({selectedParticipants, isReadOnly, didConfirm, trans
avatarID={senderWorkspace?.id}
shouldShowRightIcon={!isReadOnly && !!canUpdateSenderWorkspace}
title={senderWorkspace?.name}
icon={senderWorkspace?.avatarURL ? senderWorkspace.avatarURL : getDefaultWorkspaceAvatar(senderWorkspace?.name)}
icon={senderWorkspace?.avatarURL}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on this change? Why do we need this change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now WorkspaceAvatars calculate fallback internally (WorkspaceAvatar.tsx:35-38), so passing defaults on callsites is redundant

iconType={CONST.ICON_TYPE_WORKSPACE}
description={translate('workspace.common.workspace')}
label={translate('workspace.invoices.sendFrom')}
Expand Down
Loading
Loading