Skip to content

[HOLD] ReportActionAvatars — Layer 1 primitives + IconsAvatar#94682

Open
jmusial wants to merge 8 commits into
Expensify:mainfrom
software-mansion-labs:chore/refactor-avatar-primitives
Open

[HOLD] ReportActionAvatars — Layer 1 primitives + IconsAvatar#94682
jmusial wants to merge 8 commits into
Expensify:mainfrom
software-mansion-labs:chore/refactor-avatar-primitives

Conversation

@jmusial

@jmusial jmusial commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

This PR extracts Avatar primitives into separate file structure and unifies LHNAvatar and SearchReportAvatar into 1 thin adapter.

Fixed Issues

$ #94585

PROPOSAL:
slack

Tests

This PR is pure refactor:

A. Verify ExpenseReportListItemAvatar

  1. Log into the app
  2. Go to Home page
  3. Go to Reports page
  4. Verify that all the avatars are correct for reports

Offline tests

QA Steps

Same as tests

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Screen.Recording.2026-06-29.at.09.55.56.mov
Android: mWeb Chrome
Screen.Recording.2026-06-29.at.09.56.40.mov
iOS: Native
Screen.Recording.2026-06-29.at.09.47.38.mov
iOS: mWeb Safari
Screen.Recording.2026-06-29.at.09.49.18.mov
MacOS: Chrome / Safari
Screen.Recording.2026-06-29.at.09.38.54.mov

@codecov

codecov Bot commented Jun 26, 2026

Copy link
Copy Markdown

@jmusial

jmusial commented Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 3aaf1fd7c3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@jmusial
jmusial marked this pull request as ready for review June 29, 2026 08:11
@jmusial
jmusial requested review from a team as code owners June 29, 2026 08:11
@melvin-bot

melvin-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown

@abzokhattab Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

Comment thread src/components/Avatars/Primitives/ProfileAvatar.tsx Outdated
Comment thread src/components/Avatars/Primitives/HorizontalAvatars.tsx
@abzokhattab

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
  • I checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG)
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
iOS: HybridApp
Screen.Recording.2026-06-30.at.02.36.29.mov
iOS: mWeb Safari
Screen.Recording.2026-06-30.at.02.37.08.mov
MacOS: Chrome / Safari
Screen.Recording.2026-06-30.at.02.35.02.mov

@abzokhattab abzokhattab left a comment

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.

LGTM

@melvin-bot
melvin-bot Bot requested a review from roryabraham June 30, 2026 00:45

type DiagonalAvatarsProps = MultipleAvatarsProps & {
/** Whether to use the mid-subscript size for the avatars */
useMidSubscriptSize: 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.

Looking at this prop, it seems like it was added because we needed a size between SMALL and SMALLER. Overall I don't think that's a good component design choice - it's precisely the kind of configuration overload that makes components hard to reason about.

Instead, we should create a consistent size/scale guide for avatars.

However, looking at what we have today, we have over the years patched together some confusing semantics around this. Here's the full size ladder today:

Constant px
SMALL_SUBSCRIPT 12
MID_SUBSCRIPT / MENTION_ICON 16
SUBSCRIPT 20
SMALLER 24
SMALL 28
SMALL_NORMAL 32
HEADER 40
DEFAULT 40 (same as HEADER)
LARGE_NORMAL 48
MEDIUM 52
MEDIUM_LARGE 60
LARGE 80
LARGE_BORDERED 88
X_LARGE 100

yikes! We are mixing ordinal scale, usage-context, and hybrid names slotted between other sizes (ie.: LARGE_NORMAL is smaller than MEDIUM)

I suggest we standardize and refactor as necessary:

Size px
XXXXX_SMALL 12
XXXX_SMALL 16
XXX_SMALL 20
XX_SMALL 24
X_SMALL 28
SMALL 32
MEDIUM 40
LARGE 48
X_LARGE 52
XX_LARGE 60
XXX_LARGE 80
XXXX_LARGE 88
XXXXX_LARGE 100

and can add aliases for things like DEFAULT or HEADER that reference the standard sized listed here. It also might make sense to do this in a separate PR before this one is merged.

cc @Expensify/design

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.

Interesting idea, I don't mind it! But things like this: LARGE_BORDERED: 88 only exists because we are using the large size (80px) plus a 4px border... so that means 80 + 4px on both sides = 88. What I am saying is, your scale would be misleading, because 88 size isn't actually XXXX_LARGE, it's just XXX_LARGE with added size for borders.

That makes me think others are doing similar things. For instance, I can't even tell you where we are using 52 or 60 size avatars - so maybe there are a few we could even kill!

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.

Thx @roryabraham, making avatar sizes consistent sounds like a great idea to me, I would suggest to do it in a separate PR. That refactor touches 70 + files.

Mapping table

Old name New name px Occurrences
SMALL_SUBSCRIPT XXXXX_SMALL 12 2
MID_SUBSCRIPT XXXX_SMALL 16 18
MENTION_ICON XXXX_SMALL (merged) 16 2
SUBSCRIPT XXX_SMALL 20 2
SMALLER XX_SMALL 24 7
SMALL X_SMALL 28 24
SMALL_NORMAL SMALL 32 4
HEADER MEDIUM 40 1
DEFAULT DEFAULT (alias of MEDIUM) 40 39
LARGE_NORMAL LARGE 48 2
MEDIUM X_LARGE 52 4
MEDIUM_LARGE XX_LARGE 60 1
LARGE XXX_LARGE 80 9
LARGE_BORDERED XXXX_LARGE 88 4
X_LARGE XXXXX_LARGE 100 30
Total: 149

@shawnborton What we can potentially get rid of (using old names below):

  • SMALL_SUBSCRIPT, SUBSCRIPT, MEDIUM_LARGE- appear only as subscripts & SUBSCRIPT for split bill preview (screen)
image
  • SMALL - in practice only used once, other 3 occurances are just checks for size. The header icon size (hardcoded it there, couldn't make it appear from the flow)
image
  • LARGE_NORMAL - only used in src/components/OnboardingHelpDropdownButton and src/pages/settings/Agents/AgentInfoRow.tsx
image image
  • LARGE_BORDERED - used only when you have multiple avatars in RoomHeader (ex. Invoice send)
image

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.

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.

Definitely love the idea of cleaning some of these up - agree with Shawn that I bet a lot of these are unnecessary at this point, and even some that are being used could likely bump up or down a tiny bit to consolidate the size variables.

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.

Yeah agree with all of those points. For Agents, we should be able to kill that size once the Agents page starts using the new tables. For split, let's just reuse what we have for the attendee avatar stack that we use for the Attendees column on Spend/expense reports.

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.

For the others:

  • one occurance of SMALL can I use DEFAULT ?
  • LARGE_BORDERED -> X_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.

Yes to the first.

For the large bordered version, why do we need a version that includes the border width? Cant we define that separately? Like we need a large avatar that has 4px stroke applied to it, or whatever it is.

@jmusial jmusial Jul 3, 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.

I think it's just unfortunate naming. Most avatars have / allow for borders and they're calculated in the size. On example of LARGE (old name) the avatar would be 72, cos border width is 4.

code ref src/styles/utils/index.ts#144:

const avatarBorderWidths: Partial<Record<AvatarSizeName, number>> = {
    [CONST.AVATAR_SIZE.DEFAULT]: 3,
    [CONST.AVATAR_SIZE.SMALL_SUBSCRIPT]: 2,
    [CONST.AVATAR_SIZE.MID_SUBSCRIPT]: 2,
    [CONST.AVATAR_SIZE.SUBSCRIPT]: 2,
    [CONST.AVATAR_SIZE.SMALL]: 2,
    [CONST.AVATAR_SIZE.SMALLER]: 2,
    [CONST.AVATAR_SIZE.HEADER]: 2,
    [CONST.AVATAR_SIZE.LARGE]: 4,
    [CONST.AVATAR_SIZE.MEDIUM_LARGE]: 3,
    [CONST.AVATAR_SIZE.X_LARGE]: 4,
    [CONST.AVATAR_SIZE.MEDIUM]: 3,
    [CONST.AVATAR_SIZE.LARGE_BORDERED]: 4,
    }
    

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.

Maybe the constants for avatar size should not account for borders, and instead we just add the 4px for border width in the component during rendering if there's a border

isActive = false,
isPressed = false,
maxAvatarsInRow = CONST.AVATAR_ROW_SIZE.DEFAULT,
displayInRows: shouldDisplayAvatarsInRows = false,

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.

make sure boolean variables (including props) start with is, should, has, etc...

But in this particular PR especially, take a close look at any boolean props and consider why or if they're needed, since often they represent configuration we're trying to get away from.

I think it's fine for things that represent controlled state, such as isHovered, isActive, isPressed. I haven't looked very closely, but shouldDisplayAvatarsInRows sounds like it could actually be a separate 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.

Updated, most of the flags are used for styling @Expensify/design should we have 7 or more different border options for horizontally stacked avatars ?

code src/styles/utils/index.ts#874

function getHorizontalStackedAvatarBorderStyle({
    theme,
    isHovered,
    isPressed,
    isInReportAction = false,
    shouldUseCardBackground = false,
    isActive = false,
    customPressedBorderColor,
}: AvatarBorderStyleParams): ViewStyle {
    let borderColor = shouldUseCardBackground ? theme.cardBG : theme.appBG;

    if (isHovered) {
        borderColor = isInReportAction ? theme.hoverComponentBG : theme.border;
    }
    if (isActive) {
        borderColor = theme.messageHighlightBG;
    }

    if (isPressed) {
        borderColor = isInReportAction ? theme.hoverComponentBG : theme.buttonPressedBG;
        if (customPressedBorderColor) {
            borderColor = customPressedBorderColor;
        }
    }

    return {borderColor};
}

A splitting over shouldDisplayAvatarsInRows I don't think HorizontalAvatars is a good case for that. it only drives the 2D array split, the rest of the component logic is same. Could move it into an util and get rid of the flag though ?

@jmusial
jmusial requested a review from roryabraham June 30, 2026 13:10
/** `ProfileAvatar` wraps an `Avatar` in a pressable that navigates to the correct "view avatar" route.
* The branch it picks depends on `type` (workspace vs user) and whether a `reportID` is provided.
*/
function ProfileAvatar({shouldUseProfileNavigationWrapper, ...props}: ProfileAvatarProps) {

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.

This is a perfect example of a boolean prop that should be removed in favor of composition.

Rather than having:

<ProfileAvatar shouldUseProfileNavigationWrapper />

turn into:

<PressableWithoutFeedback {...someProps}>
    <Avatar />
</PressableWithoutFeedback

We should create a <ProfileNavigationWrapper /> component, and then compose it like so:

<ProfileAvatar.NavigationWrapper>
    <ProfileAvatar />
</ProfileAvatar.NavigationWrapper>


const {avatarID, type, name, reportID} = props;

const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE;

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 notice that since this is just wrapping Avatar, and Avatar has forking logic due to type, it's breaking down composition further up the tree.

What we've learned with these component decompositions is to actually achieve the end result we want, it's necessary to start from the bottom of the tree and work our way up.

So we should:

  1. Put this PR on HOLD
  2. Create a separate PR off main to split the lower-level Avatar component into Avatar and WorkspaceAvatar
  3. After that's merged, pick this PR back up, removing type props and the forking isWorkspace logic.

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.

Fair point, putting this one on HOLD. While checking this I noticed Icon component (Avatar uses it) could benefit from decomposition as well. Let me know what you think @roryabraham.

Draft PR for Icon refactor here.
Draft PR for Avatar refactor here, but would like to do above first.

cc. @mountiny as Icon and Avatar decomposition would need separate sub issue / or full issue if we consider migration or current call sites later.

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.

thanks, looking at those others. I think we'll definitely be best off starting from the bottom and working our way up

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.

Parent #95157

#95156 — [M1] Split Icon into primitives
#95158 — [M2] Migrate MultiGestureIcon usages
#95159 — [M3] Migrate InlineIcon usages

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.

Parent #95596

#95597 — [M1] Split Avatar into primitives
#95598 — [M2] Unify avatar sizes
#95599 — [M3] Migrate Workspace Avatar usages
#95600 — [M4] Migrate User Avatar usages

@jmusial jmusial changed the title ReportActionAvatars — Layer 1 primitives + IconsAvatar [HOLD] ReportActionAvatars — Layer 1 primitives + IconsAvatar Jul 1, 2026
@jmusial jmusial mentioned this pull request Jul 3, 2026
41 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants