Skip to content
Merged
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
55 changes: 55 additions & 0 deletions src/components/Avatar/connected/AccountAvatar.tsx

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.

Just clarifying, I think the intention is that src/Avatar/connected contains the level-two wrappers around the level-one primitives for common use-cases. And they're "connected" because they actually hook up data, rather than just displaying what they're given via props?

@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.

yup, that was my reasoning behind the name. Open to other suggestions if you feel like naming should be highlighting leveled structure more

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import SingleAvatar from '@components/Avatar/layouts/SingleAvatar';
import {usePersonalDetails} from '@components/OnyxListItemProvider';

import useDefaultAvatars from '@hooks/useDefaultAvatars';
import useStyleUtils from '@hooks/useStyleUtils';

import {buildUserIcon} from '@libs/UserAvatarUtils';

import CONST from '@src/CONST';

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

import React from 'react';

type AccountAvatarProps = {
/** Account ID of the user to display the avatar for */
accountID: number;

/** Size of the avatar */
size?: ValueOf<typeof CONST.AVATAR_SIZE>;

/** Whether to show the tooltip on hover */
shouldShowTooltip?: boolean;

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.

I think it's been an oversight of our efforts so far that shouldShowTooltip has survived our decomposition efforts. Callsites that use tooltips should include them in the JSX themselves. Callsites that don't should not.

NAB for this PR, but I think it's something we should address separately

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.

Agreed, but I'd rather do it in separate PR -> Draft stacked on this one

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.

@jmusial Use GH stack PRs system they recently introduced.

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.

would love to but AFAIK they don't work from forks :(


/** Display name used as a fallback for the avatar tooltip */
fallbackDisplayName?: string;

/** Container styles for the avatar. Replaces the size-derived default container styles when provided */
containerStyle?: StyleProp<ViewStyle>;
};

/**
* Renders a single account's avatar, resolving the icon from the personal-details context (zero Onyx subscriptions).
* Use whenever exactly one account is rendered. Pass `Avatar/UserAvatar` a `source` instead when the avatar is already resolved.
*/
function AccountAvatar({accountID, size = CONST.AVATAR_SIZE.DEFAULT, shouldShowTooltip = true, fallbackDisplayName, containerStyle}: AccountAvatarProps) {
const personalDetails = usePersonalDetails();

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.

@jmusial Do we need to subscribe to entire personal details object for a single one? Maybe use personalDetailsSelector selector for this. Since personal details object is not super deep, I would say this would optimize the component.

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.

@shubham1206agra usePersonalDetails uses context underneath so I think this should be cheaper that creating new useOnyx subscription for every avatar

const defaultAvatars = useDefaultAvatars();
const StyleUtils = useStyleUtils();

const icon = buildUserIcon({accountID, personalDetails, defaultAvatars});

return (
<SingleAvatar
avatar={icon}
size={size}
containerStyles={containerStyle ?? StyleUtils.getContainerStyles(size)}
shouldShowTooltip={shouldShowTooltip}
fallbackDisplayName={fallbackDisplayName}
/>
);
}

export default AccountAvatar;
2 changes: 1 addition & 1 deletion src/components/Avatar/layouts/SingleAvatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function SingleAvatar({avatar, size, containerStyles, shouldShowTooltip, fallbac
fallbackIcon={avatar.fallbackIcon}
fill={avatar.fill}
size={size}
testID="ReportActionAvatars-SingleAvatar"
testID="SingleAvatar"
/>
</View>
</UserDetailsTooltip>
Expand Down
7 changes: 1 addition & 6 deletions src/components/ReportActionAvatars/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,9 @@ type ReportActionAvatarsProps = {
/** Display name used as a fallback for avatar tooltip */
fallbackDisplayName?: string;

/** Invited emails to account IDs */
/** Invited emails to account IDs. Also seeds a deterministic fallback avatar for each invited account */
invitedEmailsToAccountIDs?: InvitedEmailsToAccountIDs;

/** Whether to use custom fallback avatar */
shouldUseCustomFallbackAvatar?: boolean;

/** chatReportID needed for the avatars logic. When provided, this will be used as a fallback if the snapshot is undefined */
chatReportID?: string;

Expand Down Expand Up @@ -128,7 +125,6 @@ function ReportActionAvatars({
isInReportAction = false,
fallbackDisplayName,
invitedEmailsToAccountIDs,
shouldUseCustomFallbackAvatar = false,
chatReportID,
shouldUseRealActor = false,
}: ReportActionAvatarsProps) {
Expand Down Expand Up @@ -168,7 +164,6 @@ function ReportActionAvatars({
policy: policyProp,
fallbackDisplayName,
invitedEmailsToAccountIDs,
shouldUseCustomFallbackAvatar,
chatReportID,
shouldUseRealActor,
});
Expand Down
20 changes: 9 additions & 11 deletions src/components/ReportActionAvatars/useReportActionAvatars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import usePolicy from '@hooks/usePolicy';
import useReportIsArchived from '@hooks/useReportIsArchived';

import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {addSMSDomainIfPhoneNumber} from '@libs/PhoneNumber';
import {
getDelegateAccountIDFromReportAction,
getHumanAgentAccountIDFromReportAction,
Expand All @@ -28,7 +27,7 @@ import {
isTripRoom,
shouldReportShowSubscript,
} from '@libs/ReportUtils';
import {getDefaultAvatar} from '@libs/UserAvatarUtils';
import {buildUserIcon} from '@libs/UserAvatarUtils';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand All @@ -51,7 +50,6 @@ function useReportActionAvatars({
policy: policyProp,
fallbackDisplayName = '',
invitedEmailsToAccountIDs,
shouldUseCustomFallbackAvatar = false,
chatReportID: passedChatReportID,
shouldUseRealActor = false,
}: {
Expand All @@ -64,7 +62,6 @@ function useReportActionAvatars({
policy?: OnyxInputOrEntry<Policy>;
fallbackDisplayName?: string;
invitedEmailsToAccountIDs?: InvitedEmailsToAccountIDs;
shouldUseCustomFallbackAvatar?: boolean;
chatReportID?: string;
/** When true, returns the action's real author instead of the Concierge display override used in inbox timelines. */
shouldUseRealActor?: boolean;
Expand Down Expand Up @@ -137,13 +134,14 @@ function useReportActionAvatars({

const avatarsForAccountIDs: IconType[] = accountIDsToMap.map((id) => {
const invitedEmail = invitedEmailsToAccountIDs ? Object.keys(invitedEmailsToAccountIDs).find((email) => invitedEmailsToAccountIDs[email] === id) : undefined;
return {
id,
type: CONST.ICON_TYPE_AVATAR,
source: personalDetails?.[id]?.avatar ?? defaultAvatars.FallbackAvatar,
name: personalDetails?.[id]?.[shouldUseActorAccountID ? 'displayName' : 'login'] ?? invitedEmail ?? '',
fallbackIcon: shouldUseCustomFallbackAvatar ? getDefaultAvatar({accountID: id, accountEmail: addSMSDomainIfPhoneNumber(invitedEmail ?? ''), defaultAvatars}) : undefined,
};
return buildUserIcon({
accountID: id,
personalDetails,
defaultAvatars,
invitedEmail,
// Invoice actors are named after their display name; every other icon falls back to the login inside `buildUserIcon`.
name: shouldUseActorAccountID ? (personalDetails?.[id]?.displayName ?? invitedEmail ?? '') : undefined,
});
});

const fallbackWorkspaceAvatar: IconType = {
Expand Down
18 changes: 4 additions & 14 deletions src/components/SelectionList/ListItem/TableListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import ReportActionAvatars from '@components/ReportActionAvatars';
import AccountAvatar from '@components/Avatar/connected/AccountAvatar';
import TextWithTooltip from '@components/TextWithTooltip';

import useAnimatedHighlightStyle from '@hooks/useAnimatedHighlightStyle';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';

Expand Down Expand Up @@ -43,7 +42,6 @@ function TableListItem<TItem extends ListItem>({
}: TableListItemProps<TItem>) {
const styles = useThemeStyles();
const theme = useTheme();
const StyleUtils = useStyleUtils();

const animatedHighlightStyle = useAnimatedHighlightStyle({
borderRadius: styles.selectionListPressableItemWrapper.borderRadius,
Expand All @@ -52,21 +50,13 @@ function TableListItem<TItem extends ListItem>({
backgroundColor: theme.highlightBG,
});

const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor;
const hoveredBackgroundColor = styles.sidebarLinkHover?.backgroundColor ? styles.sidebarLinkHover.backgroundColor : theme.sidebar;

const rowContent = (hovered: boolean) => (
const rowContent = () => (
<>
{!!item.accountID && (
<ReportActionAvatars
accountIDs={[item.accountID]}
<AccountAvatar
accountID={item.accountID}
fallbackDisplayName={item.text ?? item.alternateText ?? undefined}
shouldShowTooltip={showTooltip}
secondaryAvatarContainerStyle={[
StyleUtils.getBackgroundAndBorderStyle(theme.sidebar),
isFocusVisible ? StyleUtils.getBackgroundAndBorderStyle(focusedBackgroundColor) : undefined,
hovered && !isFocusVisible ? StyleUtils.getBackgroundAndBorderStyle(hoveredBackgroundColor) : undefined,
]}
/>
)}
<View style={[styles.flex1, styles.flexColumn, styles.justifyContentCenter, styles.alignItemsStretch, titleContainerStyles]}>
Expand Down
14 changes: 3 additions & 11 deletions src/components/Tables/AgentsTable/AgentsTableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import AccountAvatar from '@components/Avatar/connected/AccountAvatar';
import Button from '@components/ButtonComposed';
import Icon from '@components/Icon';
import ReportActionAvatars from '@components/ReportActionAvatars';
import type {TableRow} from '@components/Table';
import Table from '@components/Table';
import {getCellAccessibilityProps, shouldUseTableSemantics} from '@components/Table/tableAccessibility';
import TextWithTooltip from '@components/TextWithTooltip';

import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';

Expand All @@ -35,7 +34,6 @@ type AgentsTableRowProps = {
export default function AgentsTableRow({item, rowIndex, shouldUseNarrowTableLayout}: AgentsTableRowProps) {
const theme = useTheme();
const styles = useThemeStyles();
const styleUtils = useStyleUtils();
const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'ChatBubble']);

Expand All @@ -48,11 +46,6 @@ export default function AgentsTableRow({item, rowIndex, shouldUseNarrowTableLayo
const accessibilityLabel = [item.displayName, item.login].filter(Boolean).join(', ');
const selectedButtonInnerStyle = item.selected ? styles.buttonDefaultHovered : undefined;

const getSecondaryAvatarContainerStyle = (hovered: boolean) => [
styleUtils.getBackgroundAndBorderStyle(theme.sidebar),
hovered ? styleUtils.getBackgroundAndBorderStyle(styles.sidebarLinkHover?.backgroundColor ?? theme.sidebar) : undefined,
];

return (
<Table.Row
interactive
Expand All @@ -74,12 +67,11 @@ export default function AgentsTableRow({item, rowIndex, shouldUseNarrowTableLayo
style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}
{...getCellAccessibilityProps(isTableSemanticsEnabled)}
>
<ReportActionAvatars
<AccountAvatar
size={avatarSize}
accountIDs={[item.accountID]}
accountID={item.accountID}
fallbackDisplayName={item.displayName}
shouldShowTooltip
secondaryAvatarContainerStyle={getSecondaryAvatarContainerStyle(!!hovered)}
/>
<View style={[shouldUseNarrowTableLayout && styles.gap1, styles.flex1]}>
<TextWithTooltip
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import AccountAvatar from '@components/Avatar/connected/AccountAvatar';
import Badge from '@components/Badge';
import Icon from '@components/Icon';
import ReportActionAvatars from '@components/ReportActionAvatars';
import Table from '@components/Table';
import {getCellAccessibilityProps, shouldUseTableSemantics} from '@components/Table/tableAccessibility';
import TextWithTooltip from '@components/TextWithTooltip';

import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';

Expand All @@ -34,7 +33,6 @@ type DomainAdminsTableRowProps = {
export default function DomainAdminsTableRow({item, rowIndex, shouldUseNarrowTableLayout}: DomainAdminsTableRowProps) {
const theme = useTheme();
const styles = useThemeStyles();
const styleUtils = useStyleUtils();
const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']);

Expand All @@ -44,11 +42,6 @@ export default function DomainAdminsTableRow({item, rowIndex, shouldUseNarrowTab
const primaryContactLabel = item.isPrimaryContact ? translate('domain.admins.primaryContact') : '';
const accessibilityLabel = [item.name, item.email, primaryContactLabel].filter(Boolean).join(', ');

const getSecondaryAvatarContainerStyle = (hovered: boolean) => [
styleUtils.getBackgroundAndBorderStyle(theme.sidebar),
hovered ? styleUtils.getBackgroundAndBorderStyle(styles.sidebarLinkHover?.backgroundColor ?? theme.sidebar) : undefined,
];

return (
<Table.Row
interactive
Expand All @@ -69,12 +62,11 @@ export default function DomainAdminsTableRow({item, rowIndex, shouldUseNarrowTab
style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}
{...getCellAccessibilityProps(isTableSemanticsEnabled)}
>
<ReportActionAvatars
<AccountAvatar
size={avatarSize}
accountIDs={[item.accountID]}
accountID={item.accountID}
fallbackDisplayName={item.name}
shouldShowTooltip
secondaryAvatarContainerStyle={getSecondaryAvatarContainerStyle(!!hovered)}
/>
<View style={[shouldUseNarrowTableLayout && styles.gap1, styles.flex1]}>
<TextWithTooltip
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import AccountAvatar from '@components/Avatar/connected/AccountAvatar';
import Icon from '@components/Icon';
import ReportActionAvatars from '@components/ReportActionAvatars';
import Table from '@components/Table';
import {getCellAccessibilityProps, shouldUseTableSemantics} from '@components/Table/tableAccessibility';
import TextWithTooltip from '@components/TextWithTooltip';

import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';

Expand Down Expand Up @@ -35,7 +34,6 @@ type DomainMembersTableRowProps = {
export default function DomainMembersTableRow({item, rowIndex, shouldUseNarrowTableLayout, shouldShowGroupColumn}: DomainMembersTableRowProps) {
const theme = useTheme();
const styles = useThemeStyles();
const styleUtils = useStyleUtils();
const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']);

const isTableSemanticsEnabled = shouldUseTableSemantics(shouldUseNarrowTableLayout);
Expand All @@ -48,11 +46,6 @@ export default function DomainMembersTableRow({item, rowIndex, shouldUseNarrowTa
}
const accessibilityLabel = [item.name, shouldShowGroupColumn ? item.groupName : null, item.email].filter(Boolean).join(', ');

const getSecondaryAvatarContainerStyle = (hovered: boolean) => [
styleUtils.getBackgroundAndBorderStyle(theme.sidebar),
hovered ? styleUtils.getBackgroundAndBorderStyle(styles.sidebarLinkHover?.backgroundColor ?? theme.sidebar) : undefined,
];

return (
<Table.Row
interactive
Expand All @@ -73,12 +66,11 @@ export default function DomainMembersTableRow({item, rowIndex, shouldUseNarrowTa
style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}
{...getCellAccessibilityProps(isTableSemanticsEnabled)}
>
<ReportActionAvatars
<AccountAvatar
size={avatarSize}
accountIDs={[item.accountID]}
accountID={item.accountID}
fallbackDisplayName={item.name}
shouldShowTooltip
secondaryAvatarContainerStyle={getSecondaryAvatarContainerStyle(!!hovered)}
/>
<View style={[shouldUseNarrowTableLayout && styles.gap1, styles.flex1]}>
<TextWithTooltip
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import AccountAvatar from '@components/Avatar/connected/AccountAvatar';
import Icon from '@components/Icon';
import ReportActionAvatars from '@components/ReportActionAvatars';
import Table from '@components/Table';
import {getCellAccessibilityProps, shouldUseTableSemantics} from '@components/Table/tableAccessibility';
import Text from '@components/Text';
import TextWithTooltip from '@components/TextWithTooltip';

import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';

Expand All @@ -34,7 +33,6 @@ type ReportParticipantsTableRowProps = {
export default function ReportParticipantsTableRow({item, rowIndex, shouldUseNarrowTableLayout}: ReportParticipantsTableRowProps) {
const theme = useTheme();
const styles = useThemeStyles();
const styleUtils = useStyleUtils();
const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['ArrowRight']);
const isTableSemanticsEnabled = shouldUseTableSemantics(shouldUseNarrowTableLayout);
Expand All @@ -45,11 +43,6 @@ export default function ReportParticipantsTableRow({item, rowIndex, shouldUseNar
const accessibilityLabel = roleLabel ? `${item.name}, ${item.email}, ${roleLabel}` : `${item.name}, ${item.email}`;
const memberSubtitle = shouldUseNarrowTableLayout && roleLabel ? `${roleLabel} • ${item.email}` : item.email;

const getSecondaryAvatarContainerStyle = (hovered: boolean) => [
styleUtils.getBackgroundAndBorderStyle(theme.sidebar),
hovered ? styleUtils.getBackgroundAndBorderStyle(styles.sidebarLinkHover?.backgroundColor ?? theme.sidebar) : undefined,
];

return (
<Table.Row
interactive
Expand All @@ -66,11 +59,10 @@ export default function ReportParticipantsTableRow({item, rowIndex, shouldUseNar
style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}
{...getCellAccessibilityProps(isTableSemanticsEnabled)}
>
<ReportActionAvatars
<AccountAvatar
size={avatarSize}
accountIDs={[item.accountID]}
accountID={item.accountID}
fallbackDisplayName={item.name ?? item.email}
secondaryAvatarContainerStyle={getSecondaryAvatarContainerStyle(!!hovered)}
/>
<View style={[shouldUseNarrowTableLayout && styles.gap1, styles.flex1]}>
<TextWithTooltip
Expand Down
Loading
Loading