From 6241cd3130491a0b05498db2d4b20f3e68ada907 Mon Sep 17 00:00:00 2001
From: sumo_slonik <jakub.nowakowski@swmansion.com>
Date: Wed, 18 Jun 2025 16:02:02 +0200
Subject: [PATCH 01/18] Change getOneTransactionThreadReportID return value

---
 src/libs/ReportUtils.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index a8067590646f..72c51cf1059e 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -2315,7 +2315,7 @@ function hasOnlyNonReimbursableTransactions(iouReportID: string | undefined): bo
 function isOneTransactionReport(report: OnyxEntry<Report>): boolean {
     const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`] ?? ([] as ReportAction[]);
     const chatReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`];
-    return getOneTransactionThreadReportID(report, chatReport, reportActions) !== null;
+    return !!getOneTransactionThreadReportID(report, chatReport, reportActions);
 }
 
 /*

From a7eada74fbc528e2c41a580c20877df2e7a9ee92 Mon Sep 17 00:00:00 2001
From: sumo_slonik <jakub.nowakowski@swmansion.com>
Date: Wed, 18 Jun 2025 16:24:16 +0200
Subject: [PATCH 02/18] Bring back getIcons test

---
 tests/unit/ReportUtilsGetIconsTest.ts | 531 ++++++++++++++++++++++++++
 1 file changed, 531 insertions(+)
 create mode 100644 tests/unit/ReportUtilsGetIconsTest.ts

diff --git a/tests/unit/ReportUtilsGetIconsTest.ts b/tests/unit/ReportUtilsGetIconsTest.ts
new file mode 100644
index 000000000000..8625944f0f2c
--- /dev/null
+++ b/tests/unit/ReportUtilsGetIconsTest.ts
@@ -0,0 +1,531 @@
+import Onyx from 'react-native-onyx';
+import {
+    getIcons,
+    isAdminRoom,
+    isAnnounceRoom,
+    isChatReport,
+    isChatRoom,
+    isChatThread,
+    isExpenseReport,
+    isExpenseRequest,
+    isGroupChat,
+    isIndividualInvoiceRoom,
+    isInvoiceReport,
+    isInvoiceRoom,
+    isIOUReport,
+    isMoneyRequestReport,
+    isOneTransactionReport,
+    isPolicyExpenseChat,
+    isSelfDM,
+    isTaskReport,
+    isThread,
+    isWorkspaceTaskReport,
+    isWorkspaceThread,
+} from '@libs/ReportUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Report} from '@src/types/onyx';
+import * as LHNTestUtils from '../utils/LHNTestUtils';
+
+const FAKE_PERSONAL_DETAILS = LHNTestUtils.fakePersonalDetails;
+/* eslint-disable @typescript-eslint/naming-convention */
+const FAKE_REPORT_ACTIONS = {
+    [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {
+        '1': {actorAccountID: 2, actionName: CONST.REPORT.ACTIONS.TYPE.IOU},
+    },
+    [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: {
+        '2': {actorAccountID: 1, actionName: CONST.REPORT.ACTIONS.TYPE.IOU},
+    },
+    [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: {
+        '2': {
+            actorAccountID: 1,
+            actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
+            originalMessage: {
+                type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
+            },
+        },
+    },
+    // For workspace thread test - parent report actions
+    [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}workspaceParent`]: {
+        '1': {actorAccountID: 2, actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT},
+    },
+    // For multi-transaction IOU test - multiple transactions
+    [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}multiTxn`]: {
+        '1': {actorAccountID: 1, actionName: CONST.REPORT.ACTIONS.TYPE.IOU},
+        '2': {actorAccountID: 1, actionName: CONST.REPORT.ACTIONS.TYPE.IOU},
+    },
+};
+/* eslint-enable @typescript-eslint/naming-convention */
+const FAKE_REPORTS = {
+    [`${ONYXKEYS.COLLECTION.REPORT}1`]: {
+        ...LHNTestUtils.getFakeReport([1, 2], 0, true),
+        invoiceReceiver: {
+            type: CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL,
+            accountID: 3,
+        },
+    },
+    // This is the parent report for the expense request test.
+    // It MUST have type: 'expense' for the isExpenseRequest() check to pass.
+    [`${ONYXKEYS.COLLECTION.REPORT}2`]: {
+        ...LHNTestUtils.getFakeReport([1], 0, true),
+        reportID: '2',
+        type: CONST.REPORT.TYPE.EXPENSE,
+    },
+
+    // This is the parent report for the expense request test.
+    // It MUST have type: 'expense' for the isExpenseRequest() check to pass.
+    [`${ONYXKEYS.COLLECTION.REPORT}3`]: {
+        ...LHNTestUtils.getFakeReport([1], 0, true),
+        reportID: '3',
+        parentReportID: '2',
+        parentReportActionID: '2',
+        type: CONST.REPORT.TYPE.EXPENSE,
+    },
+    // Parent workspace chat for workspace thread test
+    [`${ONYXKEYS.COLLECTION.REPORT}workspaceParent`]: {
+        ...LHNTestUtils.getFakeReport([1], 0, true),
+        reportID: 'workspaceParent',
+        chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
+        policyID: '1',
+    },
+    // Parent policy expense chat for workspace task test
+    [`${ONYXKEYS.COLLECTION.REPORT}taskParent`]: {
+        ...LHNTestUtils.getFakeReport([1], 0, true),
+        reportID: 'taskParent',
+        chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
+        policyID: '1',
+    },
+    // Chat report for multi-transaction IOU test
+    [`${ONYXKEYS.COLLECTION.REPORT}chatReport`]: {
+        ...LHNTestUtils.getFakeReport([1, 2], 0, true),
+        reportID: 'chatReport',
+    },
+};
+const FAKE_POLICIES = {
+    [`${ONYXKEYS.COLLECTION.POLICY}1`]: LHNTestUtils.getFakePolicy('1'),
+};
+
+const currentUserAccountID = 5;
+
+beforeAll(() => {
+    Onyx.init({
+        keys: ONYXKEYS,
+        initialKeyStates: {
+            [ONYXKEYS.SESSION]: {email: FAKE_PERSONAL_DETAILS[currentUserAccountID]?.login, accountID: currentUserAccountID},
+            [ONYXKEYS.PERSONAL_DETAILS_LIST]: FAKE_PERSONAL_DETAILS,
+            ...FAKE_REPORT_ACTIONS,
+            ...FAKE_REPORTS,
+            ...FAKE_POLICIES,
+        },
+    });
+    // @ts-expect-error Until we add NVP_PRIVATE_DOMAINS to ONYXKEYS, we need to mock it here
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    Onyx.connect({key: ONYXKEYS.NVP_PRIVATE_DOMAINS, callback: () => {}});
+});
+
+describe('getIcons', () => {
+    it('should return a fallback icon if the report is empty', () => {
+        const report = {} as Report;
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
+        expect(icons).toHaveLength(1);
+    });
+
+    it('should return the correct icons for an expense request', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([1], 0, true),
+            parentReportID: '3',
+            parentReportActionID: '2',
+            type: CONST.REPORT.TYPE.IOU,
+        };
+        const policy = LHNTestUtils.getFakePolicy('1');
+
+        // Verify report type conditions
+        expect(isExpenseRequest(report)).toBe(true);
+        expect(isThread(report)).toBe(true);
+
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS, null, '', -1, policy);
+        expect(icons).toHaveLength(2);
+        expect(icons.at(0)?.name).toBe('Email One');
+    });
+
+    it('should return the correct icons for a chat thread', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([1], 0, true),
+            parentReportID: '1',
+            parentReportActionID: '1',
+        };
+
+        // Verify report type conditions
+        expect(isChatThread(report)).toBe(true);
+        expect(isThread(report)).toBe(true);
+
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
+        expect(icons).toHaveLength(1);
+        expect(icons.at(0)?.name).toBe('Email\u00A0Two');
+    });
+
+    it('should return the correct icons for a task report', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([1], 0, true),
+            type: CONST.REPORT.TYPE.TASK,
+            ownerAccountID: 1,
+        };
+
+        // Verify report type conditions
+        expect(isTaskReport(report)).toBe(true);
+
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
+        expect(icons).toHaveLength(1);
+        expect(icons.at(0)?.name).toBe('Email One');
+    });
+
+    it('should return the correct icons for a domain room', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([], 0, true),
+            chatType: CONST.REPORT.CHAT_TYPE.DOMAIN_ALL,
+            reportName: '#domain-test',
+        };
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
+        expect(icons).toHaveLength(1);
+        expect(icons.at(0)?.name).toBe('domain-test');
+    });
+
+    it('should return the correct icons for a policy room', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([], 0, true),
+            chatType: CONST.REPORT.CHAT_TYPE.POLICY_ROOM,
+            policyID: '1',
+        };
+        const policy = LHNTestUtils.getFakePolicy('1');
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS, null, '', -1, policy);
+        expect(icons).toHaveLength(1);
+        expect(icons.at(0)?.name).toBe('Workspace-Test-001');
+    });
+
+    it('should return the correct icons for a policy expense chat', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([1], 0, true),
+            chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
+            policyID: '1',
+        };
+        const policy = LHNTestUtils.getFakePolicy('1');
+
+        // Verify report type conditions
+        expect(isPolicyExpenseChat(report)).toBe(true);
+        expect(isChatReport(report)).toBe(true);
+
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS, null, '', -1, policy);
+        expect(icons).toHaveLength(2);
+        expect(icons.at(0)?.type).toBe(CONST.ICON_TYPE_WORKSPACE);
+        expect(icons.at(1)?.type).toBe(CONST.ICON_TYPE_AVATAR);
+    });
+
+    it('should return the correct icons for an expense report', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([1], 0, true),
+            type: CONST.REPORT.TYPE.EXPENSE,
+            policyID: '1',
+        };
+        const policy = LHNTestUtils.getFakePolicy('1');
+
+        // Verify report type conditions
+        expect(isExpenseReport(report)).toBe(true);
+
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS, null, '', -1, policy);
+        expect(icons).toHaveLength(2);
+        expect(icons.at(0)?.type).toBe(CONST.ICON_TYPE_AVATAR);
+        expect(icons.at(1)?.type).toBe(CONST.ICON_TYPE_WORKSPACE);
+    });
+
+    it('should return the correct icons for an IOU report with one transaction', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([1], 0, true),
+            reportID: '1',
+            type: CONST.REPORT.TYPE.IOU,
+            ownerAccountID: 1,
+            managerID: 2,
+        };
+
+        // Verify report type conditions
+        expect(isIOUReport(report)).toBe(true);
+        expect(isMoneyRequestReport(report)).toBe(true);
+
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
+        expect(icons).toHaveLength(1);
+        expect(icons.at(0)?.name).toBe('Email One');
+    });
+
+    it('should return the correct icons for a Self DM', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([currentUserAccountID], 0, true),
+            chatType: CONST.REPORT.CHAT_TYPE.SELF_DM,
+        };
+
+        // Verify report type conditions
+        expect(isSelfDM(report)).toBe(true);
+        expect(isChatReport(report)).toBe(true);
+
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
+        expect(icons).toHaveLength(1);
+        expect(icons.at(0)?.name).toBe('Email Five');
+    });
+
+    it('should return the correct icons for a system chat', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([], 0, true),
+            chatType: CONST.REPORT.CHAT_TYPE.SYSTEM,
+        };
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
+        expect(icons).toHaveLength(1);
+        expect(icons.at(0)?.id).toBe(CONST.ACCOUNT_ID.NOTIFICATIONS);
+    });
+
+    it('should return the correct icons for a group chat', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([1, 2, 3], 0, true),
+            chatType: CONST.REPORT.CHAT_TYPE.GROUP,
+        };
+
+        // Verify report type conditions
+        expect(isGroupChat(report)).toBe(true);
+        expect(isChatReport(report)).toBe(true);
+
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
+        expect(icons).toHaveLength(1);
+    });
+
+    it('should return the correct icons for a group chat without an avatar', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([1, 2, 3], 0, true),
+            chatType: CONST.REPORT.CHAT_TYPE.GROUP,
+        };
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
+        expect(icons).toHaveLength(1);
+    });
+
+    it('should return the correct icons for a group chat with an avatar', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([1, 2, 3], 0, true),
+            chatType: CONST.REPORT.CHAT_TYPE.GROUP,
+            avatarUrl: 'https://example.com/avatar.png',
+        };
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
+        expect(icons).toHaveLength(1);
+    });
+
+    it('should return the correct icons for an invoice report', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([], 0, true),
+            type: CONST.REPORT.TYPE.INVOICE,
+            chatReportID: '1',
+            policyID: '1',
+        };
+        const policy = LHNTestUtils.getFakePolicy('1');
+
+        // Verify report type conditions
+        expect(isInvoiceReport(report)).toBe(true);
+
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS, null, '', -1, policy);
+        expect(icons).toHaveLength(2);
+        expect(icons.at(0)?.type).toBe(CONST.ICON_TYPE_WORKSPACE);
+        expect(icons.at(1)?.name).toBe('Email Three');
+    });
+
+    it('should return all participant icons for a one-on-one chat', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([1], 0, true),
+        };
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
+        expect(icons).toHaveLength(1);
+        expect(icons.at(0)?.name).toBe('Email One');
+    });
+
+    it('should return all participant icons as a fallback', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([1, 2, 3, 4], 0, true),
+            type: 'some_random_type',
+        };
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
+        expect(icons).toHaveLength(4);
+    });
+
+    it('should return the correct icons for a workspace thread', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([1], 0, true),
+            parentReportID: 'workspaceParent',
+            parentReportActionID: '1',
+            policyID: '1',
+            type: CONST.REPORT.TYPE.CHAT,
+            chatType: CONST.REPORT.CHAT_TYPE.POLICY_EXPENSE_CHAT,
+        };
+        const policy = LHNTestUtils.getFakePolicy('1');
+
+        // Verify report type conditions
+        expect(isChatThread(report)).toBe(true);
+        expect(isWorkspaceThread(report)).toBe(true);
+
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS, null, '', -1, policy);
+
+        expect(icons).toHaveLength(2); // Actor + workspace icon
+        expect(icons.at(0)?.type).toBe(CONST.ICON_TYPE_AVATAR);
+        expect(icons.at(1)?.type).toBe(CONST.ICON_TYPE_WORKSPACE);
+    });
+
+    it('should return the correct icons for a workspace task report', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([1], 0, true),
+            type: CONST.REPORT.TYPE.TASK,
+            ownerAccountID: 1,
+            parentReportID: 'taskParent',
+            policyID: '1',
+        };
+        const policy = LHNTestUtils.getFakePolicy('1');
+
+        // Verify report type conditions
+        expect(isTaskReport(report)).toBe(true);
+        expect(isWorkspaceTaskReport(report)).toBe(true);
+
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS, null, '', -1, policy);
+
+        expect(icons).toHaveLength(2); // Owner + workspace icon
+        expect(icons.at(0)?.type).toBe(CONST.ICON_TYPE_AVATAR);
+        expect(icons.at(1)?.type).toBe(CONST.ICON_TYPE_WORKSPACE);
+    });
+
+    it('should return the correct icons for an admin room', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([], 0, true),
+            chatType: CONST.REPORT.CHAT_TYPE.POLICY_ADMINS,
+            policyID: '1',
+        };
+        const policy = LHNTestUtils.getFakePolicy('1');
+
+        // Verify report type conditions
+        expect(isAdminRoom(report)).toBe(true);
+        expect(isChatRoom(report)).toBe(true);
+
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS, null, '', -1, policy);
+        expect(icons).toHaveLength(1);
+        expect(icons.at(0)?.type).toBe(CONST.ICON_TYPE_WORKSPACE);
+    });
+
+    it('should return the correct icons for an announce room', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([], 0, true),
+            chatType: CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE,
+            policyID: '1',
+        };
+        const policy = LHNTestUtils.getFakePolicy('1');
+
+        // Verify report type conditions
+        expect(isAnnounceRoom(report)).toBe(true);
+        expect(isChatRoom(report)).toBe(true);
+
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS, null, '', -1, policy);
+        expect(icons).toHaveLength(1);
+        expect(icons.at(0)?.type).toBe(CONST.ICON_TYPE_WORKSPACE);
+    });
+
+    it('should return the correct icons for an invoice room with individual receiver', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([], 0, true),
+            chatType: CONST.REPORT.CHAT_TYPE.INVOICE,
+            policyID: '1',
+            invoiceReceiver: {
+                type: CONST.REPORT.INVOICE_RECEIVER_TYPE.INDIVIDUAL,
+                accountID: 2,
+            },
+        };
+        const policy = LHNTestUtils.getFakePolicy('1');
+
+        // Verify report type conditions
+        expect(isInvoiceRoom(report)).toBe(true);
+        expect(isIndividualInvoiceRoom(report)).toBe(true);
+
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS, null, '', -1, policy);
+        expect(icons).toHaveLength(2); // Workspace + individual receiver
+        expect(icons.at(0)?.type).toBe(CONST.ICON_TYPE_WORKSPACE);
+        expect(icons.at(1)?.type).toBe(CONST.ICON_TYPE_AVATAR);
+    });
+
+    it('should return the correct icons for an invoice room with business receiver', () => {
+        const receiverPolicy = LHNTestUtils.getFakePolicy('2', 'Receiver-Policy');
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([], 0, true),
+            chatType: CONST.REPORT.CHAT_TYPE.INVOICE,
+            policyID: '1',
+            invoiceReceiver: {
+                type: CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS,
+                policyID: '2',
+            },
+        };
+        const policy = LHNTestUtils.getFakePolicy('1');
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS, null, '', -1, policy, receiverPolicy);
+        expect(icons).toHaveLength(2); // Workspace + receiver workspace
+        expect(icons.at(0)?.type).toBe(CONST.ICON_TYPE_WORKSPACE);
+        expect(icons.at(1)?.type).toBe(CONST.ICON_TYPE_WORKSPACE);
+        expect(icons.at(1)?.name).toBe('Receiver-Policy');
+    });
+
+    it('should return the correct icons for a multi-transaction IOU report where current user is not manager', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([1, 2], 0, true),
+            reportID: 'multiTxn',
+            chatReportID: 'chatReport',
+            type: CONST.REPORT.TYPE.IOU,
+            ownerAccountID: 1,
+            managerID: 2, // Different from current user (5)
+            // eslint-disable-next-line @typescript-eslint/naming-convention
+            participants: {
+                // eslint-disable-next-line @typescript-eslint/naming-convention
+                '1': {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS},
+                // eslint-disable-next-line @typescript-eslint/naming-convention
+                '2': {notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS},
+            },
+        };
+
+        // Verify report type conditions
+        expect(isIOUReport(report)).toBe(true);
+        expect(isMoneyRequestReport(report)).toBe(true);
+        expect(isOneTransactionReport(report)).toBe(false);
+
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
+
+        // https://github.com/Expensify/App/issues/64333
+        expect(icons).toHaveLength(2);
+        expect(icons.at(0)?.name).toBe('Email\u0020One');
+        expect(icons.at(1)?.name).toBe('Email\u0020Two');
+    });
+
+    it('should return the correct icons for a concierge chat', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([CONST.ACCOUNT_ID.CONCIERGE], 0, true),
+            participants: {
+                [CONST.ACCOUNT_ID.CONCIERGE]: {
+                    notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
+                },
+            },
+        };
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
+        expect(icons).toHaveLength(1);
+        expect(icons.at(0)?.id).toBe(CONST.ACCOUNT_ID.CONCIERGE);
+    });
+
+    it('should return the correct icons for an invoice report with individual receiver', () => {
+        const report: Report = {
+            ...LHNTestUtils.getFakeReport([], 0, true),
+            type: CONST.REPORT.TYPE.INVOICE,
+            chatReportID: '1',
+            policyID: '1',
+        };
+
+        // Verify report type conditions
+        expect(isInvoiceReport(report)).toBe(true);
+
+        const policy = LHNTestUtils.getFakePolicy('1');
+        const icons = getIcons(report, FAKE_PERSONAL_DETAILS, null, '', -1, policy);
+        expect(icons).toHaveLength(2);
+        expect(icons.at(0)?.type).toBe(CONST.ICON_TYPE_WORKSPACE);
+        expect(icons.at(1)?.name).toBe('Email Three');
+    });
+});

From 51e0bf6ee12c0ca6b4d7d74850b211f9d454f39b Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Tue, 24 Jun 2025 09:11:57 +0200
Subject: [PATCH 03/18] Fix two tests & remove isOneTransactionReport export

---
 src/libs/ReportUtils.ts                       |  1 -
 .../EnforceActionExportRestrictions.ts        |  9 +++---
 tests/unit/ReportUtilsGetIconsTest.ts         | 32 +++++++++----------
 3 files changed, 19 insertions(+), 23 deletions(-)

diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 72c51cf1059e..daa29e13134d 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -11462,7 +11462,6 @@ export {
     hasReportBeenReopened,
     getMoneyReportPreviewName,
     getNextApproverAccountID,
-    isOneTransactionReport,
     isWorkspaceTaskReport,
     isWorkspaceThread,
 };
diff --git a/tests/actions/EnforceActionExportRestrictions.ts b/tests/actions/EnforceActionExportRestrictions.ts
index c015aba6e91b..52760e9048c0 100644
--- a/tests/actions/EnforceActionExportRestrictions.ts
+++ b/tests/actions/EnforceActionExportRestrictions.ts
@@ -17,11 +17,10 @@ describe('ReportUtils', () => {
         expect(ReportUtils.getReport).toBeUndefined();
     });
 
-    // TODO: Re-enable this test when isOneTransactionReport is fixed https://github.com/Expensify/App/issues/64333
-    // it('does not export isOneTransactionReport', () => {
-    //     // @ts-expect-error the test is asserting that it's undefined, so the TS error is normal
-    //     expect(ReportUtils.isOneTransactionReport).toBeUndefined();
-    // });
+    it('does not export isOneTransactionReport', () => {
+        // @ts-expect-error the test is asserting that it's undefined, so the TS error is normal
+        expect(ReportUtils.isOneTransactionReport).toBeUndefined();
+    });
 
     it('does not export getPolicy', () => {
         // @ts-expect-error the test is asserting that it's undefined, so the TS error is normal
diff --git a/tests/unit/ReportUtilsGetIconsTest.ts b/tests/unit/ReportUtilsGetIconsTest.ts
index 8625944f0f2c..7bb9e86ba813 100644
--- a/tests/unit/ReportUtilsGetIconsTest.ts
+++ b/tests/unit/ReportUtilsGetIconsTest.ts
@@ -1,4 +1,6 @@
+import type {OnyxCollection} from 'react-native-onyx';
 import Onyx from 'react-native-onyx';
+import {getOneTransactionThreadReportID} from '@libs/ReportActionsUtils';
 import {
     getIcons,
     isAdminRoom,
@@ -14,7 +16,6 @@ import {
     isInvoiceRoom,
     isIOUReport,
     isMoneyRequestReport,
-    isOneTransactionReport,
     isPolicyExpenseChat,
     isSelfDM,
     isTaskReport,
@@ -24,35 +25,30 @@ import {
 } from '@libs/ReportUtils';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
-import type {Report} from '@src/types/onyx';
+import type {Report, ReportAction} from '@src/types/onyx';
+import {actionR14932, actionR98765} from '../../__mocks__/reportData/actions';
 import * as LHNTestUtils from '../utils/LHNTestUtils';
 
 const FAKE_PERSONAL_DETAILS = LHNTestUtils.fakePersonalDetails;
 /* eslint-disable @typescript-eslint/naming-convention */
-const FAKE_REPORT_ACTIONS = {
+const FAKE_REPORT_ACTIONS: OnyxCollection<Record<string, ReportAction>> = {
     [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}1`]: {
-        '1': {actorAccountID: 2, actionName: CONST.REPORT.ACTIONS.TYPE.IOU},
+        '1': {...actionR14932, actorAccountID: 2},
     },
     [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}2`]: {
-        '2': {actorAccountID: 1, actionName: CONST.REPORT.ACTIONS.TYPE.IOU},
+        '2': {...actionR98765, actorAccountID: 1},
     },
     [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}3`]: {
-        '2': {
-            actorAccountID: 1,
-            actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
-            originalMessage: {
-                type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
-            },
-        },
+        '2': {...actionR98765, actorAccountID: 1},
     },
     // For workspace thread test - parent report actions
     [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}workspaceParent`]: {
-        '1': {actorAccountID: 2, actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT},
+        '1': {...actionR14932, actorAccountID: 2, actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT},
     },
     // For multi-transaction IOU test - multiple transactions
     [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}multiTxn`]: {
-        '1': {actorAccountID: 1, actionName: CONST.REPORT.ACTIONS.TYPE.IOU},
-        '2': {actorAccountID: 1, actionName: CONST.REPORT.ACTIONS.TYPE.IOU},
+        '1': {...actionR14932, actorAccountID: 1},
+        '2': {...actionR98765, actorAccountID: 1},
     },
 };
 /* eslint-enable @typescript-eslint/naming-convention */
@@ -484,14 +480,16 @@ describe('getIcons', () => {
             },
         };
 
+        const reportActions = FAKE_REPORT_ACTIONS?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`];
+        const chatReport = FAKE_REPORTS?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`];
+
         // Verify report type conditions
         expect(isIOUReport(report)).toBe(true);
         expect(isMoneyRequestReport(report)).toBe(true);
-        expect(isOneTransactionReport(report)).toBe(false);
+        expect(getOneTransactionThreadReportID(report, chatReport, reportActions)).toBeFalsy();
 
         const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
 
-        // https://github.com/Expensify/App/issues/64333
         expect(icons).toHaveLength(2);
         expect(icons.at(0)?.name).toBe('Email\u0020One');
         expect(icons.at(1)?.name).toBe('Email\u0020Two');

From d0054c7e939258ab154bc437fd63805d4c155e92 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Tue, 24 Jun 2025 14:34:54 +0200
Subject: [PATCH 04/18] Display sender avatar next to report preview with only
 one person expenses

---
 .../home/report/ReportActionItemSingle.tsx    | 23 +++++++++++++++----
 1 file changed, 19 insertions(+), 4 deletions(-)

diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index 1abbe2ac0ff4..6b655d844e89 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -21,7 +21,7 @@ import ControlSelection from '@libs/ControlSelection';
 import DateUtils from '@libs/DateUtils';
 import Navigation from '@libs/Navigation/Navigation';
 import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
-import {getManagerOnVacation, getReportActionMessage, getSubmittedTo, getVacationer} from '@libs/ReportActionsUtils';
+import {getManagerOnVacation, getReportActionMessage, getSendMoneyFlowOneTransactionThreadID, getSubmittedTo, getVacationer, isMoneyRequestAction} from '@libs/ReportActionsUtils';
 import {
     getDefaultWorkspaceAvatar,
     getDisplayNameForParticipant,
@@ -29,6 +29,7 @@ import {
     getPolicyName,
     getReportActionActorAccountID,
     getWorkspaceIcon,
+    isActionCreator,
     isIndividualInvoiceRoom,
     isInvoiceReport as isInvoiceReportUtils,
     isInvoiceRoom,
@@ -111,17 +112,31 @@ function ReportActionItemSingle({
     const [innerPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {
         canBeMissing: true,
     });
+    const [iouActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, {
+        canBeMissing: true,
+        selector: (actions) => Object.values(actions ?? {}).filter(isMoneyRequestAction),
+    });
+
     const activePolicies = policies ?? innerPolicies;
     const policy = usePolicy(report?.policyID);
     const delegatePersonalDetails = action?.delegateAccountID ? personalDetails?.[action?.delegateAccountID] : undefined;
     const ownerAccountID = iouReport?.ownerAccountID ?? action?.childOwnerAccountID;
     const isReportPreviewAction = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW;
     const actorAccountID = getReportActionActorAccountID(action, iouReport, report, delegatePersonalDetails);
+
+    // We want to display only the sender's avatar next to the report preview if it only contains one person's expenses.
+    const areIOUActionsOnlyFromOnePerson = isReportPreviewAction && iouActions?.length && (iouActions.every(isActionCreator) || !iouActions.some(isActionCreator));
+
+    const isSendMoneyFlow = !!getSendMoneyFlowOneTransactionThreadID(iouActions, report);
+
+    // We need to change the account ID to get the avatar & name of the report owner. The only exception is the 'Send Money' flow, where the avatar is retrieved correctly. We just need to hide the second one.
+    const accountID = areIOUActionsOnlyFromOnePerson && !isSendMoneyFlow ? ownerAccountID : actorAccountID;
+
     const invoiceReceiverPolicy =
         report?.invoiceReceiver && 'policyID' in report.invoiceReceiver ? activePolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.invoiceReceiver.policyID}`] : undefined;
 
-    let displayName = getDisplayNameForParticipant({accountID: actorAccountID, personalDetailsData: personalDetails});
-    const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails?.[actorAccountID ?? CONST.DEFAULT_NUMBER_ID] ?? {};
+    let displayName = getDisplayNameForParticipant({accountID, personalDetailsData: personalDetails});
+    const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails?.[accountID ?? CONST.DEFAULT_NUMBER_ID] ?? {};
     const accountOwnerDetails = getPersonalDetailByEmail(login ?? '');
 
     // Vacation delegate details for submitted action
@@ -137,7 +152,7 @@ function ReportActionItemSingle({
     // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
     let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '');
     const isTripRoom = isTripRoomReportUtils(report);
-    const displayAllActors = isReportPreviewAction && !isTripRoom && !isPolicyExpenseChat(report);
+    const displayAllActors = isReportPreviewAction && !isTripRoom && !isPolicyExpenseChat(report) && !areIOUActionsOnlyFromOnePerson;
     const isInvoiceReport = isInvoiceReportUtils(iouReport ?? null);
     const isWorkspaceActor = isInvoiceReport || (isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors));
 

From ff0979b417f8972c0d0a33779bd6c46ba735ab81 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Wed, 25 Jun 2025 07:58:18 +0200
Subject: [PATCH 05/18] Fix ReportUtilsGetIconsTest test case

---
 tests/unit/ReportUtilsGetIconsTest.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/unit/ReportUtilsGetIconsTest.ts b/tests/unit/ReportUtilsGetIconsTest.ts
index 7bb9e86ba813..05c35d317733 100644
--- a/tests/unit/ReportUtilsGetIconsTest.ts
+++ b/tests/unit/ReportUtilsGetIconsTest.ts
@@ -339,7 +339,7 @@ describe('getIcons', () => {
     it('should return all participant icons as a fallback', () => {
         const report: Report = {
             ...LHNTestUtils.getFakeReport([1, 2, 3, 4], 0, true),
-            type: 'some_random_type',
+            type: undefined,
         };
         const icons = getIcons(report, FAKE_PERSONAL_DETAILS);
         expect(icons).toHaveLength(4);

From 78292a29b0f659e81f6970a086aba7e3546085e5 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Thu, 26 Jun 2025 13:39:53 +0200
Subject: [PATCH 06/18] Add c3024 suggestion

---
 src/pages/home/report/ReportActionItemSingle.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index 6b655d844e89..15f903936126 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -125,7 +125,7 @@ function ReportActionItemSingle({
     const actorAccountID = getReportActionActorAccountID(action, iouReport, report, delegatePersonalDetails);
 
     // We want to display only the sender's avatar next to the report preview if it only contains one person's expenses.
-    const areIOUActionsOnlyFromOnePerson = isReportPreviewAction && iouActions?.length && (iouActions.every(isActionCreator) || !iouActions.some(isActionCreator));
+    const areIOUActionsOnlyFromOnePerson = isReportPreviewAction && !!iouActions?.length && (iouActions.every(isActionCreator) || !iouActions.some(isActionCreator));
 
     const isSendMoneyFlow = !!getSendMoneyFlowOneTransactionThreadID(iouActions, report);
 

From b16e39a2e37e331c81d7f28528f2863b38b53717 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Fri, 27 Jun 2025 16:27:23 +0200
Subject: [PATCH 07/18] Add useIDOfReportPreviewSender & tests for it

---
 .../home/report/ReportActionItemSingle.tsx    |  78 +++++++++--
 tests/unit/ReportActionItemSingleTest.ts      | 122 ++++++++++++++++--
 2 files changed, 175 insertions(+), 25 deletions(-)

diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index 5015d534a360..1939397f4269 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -19,9 +19,10 @@ import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import ControlSelection from '@libs/ControlSelection';
 import DateUtils from '@libs/DateUtils';
+import {selectAllTransactionsForReport} from '@libs/MoneyRequestReportUtils';
 import Navigation from '@libs/Navigation/Navigation';
 import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
-import {getReportActionMessage, getSendMoneyFlowOneTransactionThreadID, isMoneyRequestAction} from '@libs/ReportActionsUtils';
+import {getOriginalMessage, getReportActionMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
 import {
     getDefaultWorkspaceAvatar,
     getDisplayNameForParticipant,
@@ -89,6 +90,62 @@ const showWorkspaceDetails = (reportID: string | undefined) => {
     Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID, Navigation.getReportRHPActiveRoute()));
 };
 
+/**
+ * This hook is used to determine the ID of the sender for the report preview action.
+ * There are few fallbacks to determine the sender ID.
+ *
+ * It should not be used outside of this component.
+ * The only reason it is exported to a hook is to make it easier to remove it in the future.
+ *
+ * For a reason why it is here, see https://github.com/Expensify/App/pull/64802 discussion
+ */
+function useIDOfReportPreviewSender({action, iouReport}: {action: OnyxEntry<ReportAction>; iouReport?: OnyxEntry<Report>}) {
+    const [iouActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, {
+        canBeMissing: true,
+        selector: (actions) => Object.values(actions ?? {}).filter(isMoneyRequestAction),
+    });
+
+    const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
+        canBeMissing: true,
+        selector: (allTransactions) => selectAllTransactionsForReport(allTransactions, action?.childReportID, iouActions ?? []),
+    });
+
+    if (action?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) {
+        return;
+    }
+
+    /* We need to perform the following checks to determine if the report preview is a single avatar: */
+
+    // 1. If all actions are created by one person - either all actions are created by the current user or all actions are created by the user we have opened chat with.
+    const areActionsCreatedByOnePerson = iouActions?.every(isActionCreator) || !iouActions?.some(isActionCreator);
+
+    // 2. If all amounts have the same sign - either all amounts are positive or all amounts are negative.
+    // We have to do this because there can be a case when actions are not available
+    // See: https://github.com/Expensify/App/pull/64802#issuecomment-3008944401
+    const areAmountsSignsTheSame = new Set(transactions?.map((tr) => Math.sign(tr.amount))).size < 2;
+
+    // 3. If there is only one attendee - we check that by counting unique emails in the attendees list.
+    // This has to be done as there are cases when transaction amounts signs are the same until report is opened.
+    // See: https://github.com/Expensify/App/pull/64802#issuecomment-3007906310
+    const isThereOnlyOneAttendee =
+        new Set(
+            transactions
+                // If the transaction is a split, then it had to be created by someone even though the attendees are not present.
+                ?.flatMap<string | boolean>((tr) => tr.comment?.attendees?.map((att) => att.email) ?? tr.comment?.source === 'split')
+                // We filter out the concierge email and empty emails.
+                .filter((email) => !!email && email !== CONST.EMAIL.CONCIERGE),
+        ).size <= 1;
+
+    // If the action is a 'Send Money' flow, it will only have one transaction, but the person who sent the money is the child manager account, not the child owner account.
+    const isSendMoneyFlow = action?.childMoneyRequestCount === 0 && transactions?.length === 1;
+    const singleAvatarAccountID = isSendMoneyFlow ? action.childManagerAccountID : action?.childOwnerAccountID;
+
+    // If we have more than one IOU action, but they are 'Pay' actions, we consider it as a single avatar. The only place where 'Pay' actions are considered valid is in the 'Send Money' flow.
+    const areThereValidIOUActions = !!iouActions?.length && (isSendMoneyFlow || iouActions.some((iouAction) => getOriginalMessage(iouAction)?.type !== CONST.IOU.REPORT_ACTION_TYPE.PAY));
+
+    return (!areThereValidIOUActions || areActionsCreatedByOnePerson) && areAmountsSignsTheSame && isThereOnlyOneAttendee ? singleAvatarAccountID : undefined;
+}
+
 function ReportActionItemSingle({
     action,
     children,
@@ -112,10 +169,6 @@ function ReportActionItemSingle({
     const [innerPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {
         canBeMissing: true,
     });
-    const [iouActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, {
-        canBeMissing: true,
-        selector: (actions) => Object.values(actions ?? {}).filter(isMoneyRequestAction),
-    });
 
     const activePolicies = policies ?? innerPolicies;
     const policy = usePolicy(report?.policyID);
@@ -123,14 +176,8 @@ function ReportActionItemSingle({
     const ownerAccountID = iouReport?.ownerAccountID ?? action?.childOwnerAccountID;
     const isReportPreviewAction = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW;
     const actorAccountID = getReportActionActorAccountID(action, iouReport, report, delegatePersonalDetails);
-
-    // We want to display only the sender's avatar next to the report preview if it only contains one person's expenses.
-    const areIOUActionsOnlyFromOnePerson = isReportPreviewAction && !!iouActions?.length && (iouActions.every(isActionCreator) || !iouActions.some(isActionCreator));
-
-    const isSendMoneyFlow = !!getSendMoneyFlowOneTransactionThreadID(iouActions, report);
-
-    // We need to change the account ID to get the avatar & name of the report owner. The only exception is the 'Send Money' flow, where the avatar is retrieved correctly. We just need to hide the second one.
-    const accountID = areIOUActionsOnlyFromOnePerson && !isSendMoneyFlow ? ownerAccountID : actorAccountID;
+    const reportPreviewSenderID = useIDOfReportPreviewSender({action, iouReport});
+    const accountID = reportPreviewSenderID ?? actorAccountID ?? CONST.DEFAULT_NUMBER_ID;
 
     const invoiceReceiverPolicy =
         report?.invoiceReceiver && 'policyID' in report.invoiceReceiver ? activePolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.invoiceReceiver.policyID}`] : undefined;
@@ -142,7 +189,9 @@ function ReportActionItemSingle({
     // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
     let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '');
     const isTripRoom = isTripRoomReportUtils(report);
-    const displayAllActors = isReportPreviewAction && !isTripRoom && !isPolicyExpenseChat(report) && !areIOUActionsOnlyFromOnePerson;
+
+    // We want to display only the sender's avatar next to the report preview if it only contains one person's expenses.
+    const displayAllActors = isReportPreviewAction && !isTripRoom && !isPolicyExpenseChat(report) && !reportPreviewSenderID;
     const isInvoiceReport = isInvoiceReportUtils(iouReport ?? null);
     const isWorkspaceActor = isInvoiceReport || (isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors));
 
@@ -372,3 +421,4 @@ function ReportActionItemSingle({
 ReportActionItemSingle.displayName = 'ReportActionItemSingle';
 
 export default ReportActionItemSingle;
+export {useIDOfReportPreviewSender as DO_NOT_USE__EXPORT_FOR_TESTS__useIDOfReportPreviewSender};
diff --git a/tests/unit/ReportActionItemSingleTest.ts b/tests/unit/ReportActionItemSingleTest.ts
index b02f56b38d51..37e5f9fa4897 100644
--- a/tests/unit/ReportActionItemSingleTest.ts
+++ b/tests/unit/ReportActionItemSingleTest.ts
@@ -1,22 +1,17 @@
-import {screen, waitFor} from '@testing-library/react-native';
+import {renderHook, screen, waitFor} from '@testing-library/react-native';
 import Onyx from 'react-native-onyx';
 import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import {DO_NOT_USE__EXPORT_FOR_TESTS__useIDOfReportPreviewSender as useIDOfReportPreviewSender} from '@src/pages/home/report/ReportActionItemSingle';
 import type {PersonalDetailsList} from '@src/types/onyx';
 import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet';
+import {actionR14932, actionR98765} from '../../__mocks__/reportData/actions';
+import {chatReportR14932, iouReportR14932} from '../../__mocks__/reportData/reports';
+import {transactionR14932} from '../../__mocks__/reportData/transactions';
 import * as LHNTestUtils from '../utils/LHNTestUtils';
 import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
 import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates';
 
-const ONYXKEYS = {
-    PERSONAL_DETAILS_LIST: 'personalDetailsList',
-    IS_LOADING_REPORT_DATA: 'isLoadingReportData',
-    COLLECTION: {
-        REPORT_ACTIONS: 'reportActions_',
-        POLICY: 'policy_',
-    },
-    NETWORK: 'network',
-} as const;
-
 describe('ReportActionItemSingle', () => {
     beforeAll(() =>
         Onyx.init({
@@ -89,3 +84,108 @@ describe('ReportActionItemSingle', () => {
         });
     });
 });
+
+const reportActions = [{[actionR14932.reportActionID]: actionR14932}];
+const transactions = [transactionR14932];
+
+const transactionCollectionDataSet = toCollectionDataSet(ONYXKEYS.COLLECTION.TRANSACTION, transactions, (transaction) => transaction.transactionID);
+const reportActionCollectionDataSet = toCollectionDataSet(ONYXKEYS.COLLECTION.REPORT_ACTIONS, reportActions, (actions) => Object.values(actions).at(0)?.childReportID);
+
+const validAction = {
+    ...actionR98765,
+    childReportID: iouReportR14932.reportID,
+    actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW,
+    childOwnerAccountID: iouReportR14932.ownerAccountID,
+    childManagerAccountID: iouReportR14932.managerID,
+};
+
+describe('useIDOfReportPreviewSender', () => {
+    beforeAll(() => {
+        Onyx.init({
+            keys: ONYXKEYS,
+        });
+    });
+
+    beforeEach(() => {
+        Onyx.multiSet({
+            ...reportActionCollectionDataSet,
+            ...transactionCollectionDataSet,
+        });
+        return waitForBatchedUpdates();
+    });
+
+    afterEach(() => {
+        Onyx.clear();
+        return waitForBatchedUpdates();
+    });
+
+    it('returns undefined when action is not a report preview', () => {
+        const {result} = renderHook(() =>
+            useIDOfReportPreviewSender({
+                action: actionR14932,
+                iouReport: iouReportR14932,
+            }),
+        );
+        expect(result.current).toBeUndefined();
+    });
+
+    it('returns childManagerAccountID when all conditions are met for Send Money flow', async () => {
+        const {result} = renderHook(() =>
+            useIDOfReportPreviewSender({
+                action: {...validAction, childMoneyRequestCount: 0},
+                iouReport: iouReportR14932,
+            }),
+        );
+        expect(result.current).toBe(iouReportR14932.managerID);
+    });
+
+    it('returns undefined when there are multiple attendees', async () => {
+        await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}`, {
+            ...transactionR14932,
+            comment: {
+                attendees: [{email: 'test@test.com', displayName: 'Test One', avatarUrl: 'https://none.com/none'}],
+            },
+        });
+        await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}2`, {
+            ...transactionR14932,
+            comment: {
+                attendees: [{email: 'test2@test.com', displayName: 'Test Two', avatarUrl: 'https://none.com/none2'}],
+            },
+        });
+        const {result} = renderHook(() =>
+            useIDOfReportPreviewSender({
+                action: validAction,
+                iouReport: iouReportR14932,
+            }),
+        );
+        expect(result.current).toBeUndefined();
+    });
+
+    it('returns undefined when amounts have different signs', async () => {
+        await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}`, {
+            ...transactionR14932,
+            amount: 100,
+        });
+        await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}2`, {
+            ...transactionR14932,
+            amount: -100,
+        });
+        const {result} = renderHook(() =>
+            useIDOfReportPreviewSender({
+                action: validAction,
+                iouReport: iouReportR14932,
+            }),
+        );
+        expect(result.current).toBeUndefined();
+    });
+
+    it('returns childOwnerAccountID when all conditions are met', () => {
+        const {result} = renderHook(() =>
+            useIDOfReportPreviewSender({
+                action: validAction,
+                iouReport: iouReportR14932,
+            }),
+        );
+        expect(result.current).toBe(iouReportR14932.ownerAccountID);
+    });
+});

From 78220d81c43a1329c20254feb872f41775c6ed91 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Fri, 27 Jun 2025 16:33:37 +0200
Subject: [PATCH 08/18] Fix ESLint checks

---
 src/pages/home/report/ReportActionItemSingle.tsx | 1 +
 tests/unit/ReportActionItemSingleTest.ts         | 4 ++--
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index 1939397f4269..66049afd576d 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -117,6 +117,7 @@ function useIDOfReportPreviewSender({action, iouReport}: {action: OnyxEntry<Repo
     /* We need to perform the following checks to determine if the report preview is a single avatar: */
 
     // 1. If all actions are created by one person - either all actions are created by the current user or all actions are created by the user we have opened chat with.
+    // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
     const areActionsCreatedByOnePerson = iouActions?.every(isActionCreator) || !iouActions?.some(isActionCreator);
 
     // 2. If all amounts have the same sign - either all amounts are positive or all amounts are negative.
diff --git a/tests/unit/ReportActionItemSingleTest.ts b/tests/unit/ReportActionItemSingleTest.ts
index 37e5f9fa4897..cfe3b893b9a9 100644
--- a/tests/unit/ReportActionItemSingleTest.ts
+++ b/tests/unit/ReportActionItemSingleTest.ts
@@ -6,7 +6,7 @@ import {DO_NOT_USE__EXPORT_FOR_TESTS__useIDOfReportPreviewSender as useIDOfRepor
 import type {PersonalDetailsList} from '@src/types/onyx';
 import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet';
 import {actionR14932, actionR98765} from '../../__mocks__/reportData/actions';
-import {chatReportR14932, iouReportR14932} from '../../__mocks__/reportData/reports';
+import {iouReportR14932} from '../../__mocks__/reportData/reports';
 import {transactionR14932} from '../../__mocks__/reportData/transactions';
 import * as LHNTestUtils from '../utils/LHNTestUtils';
 import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
@@ -129,7 +129,7 @@ describe('useIDOfReportPreviewSender', () => {
         expect(result.current).toBeUndefined();
     });
 
-    it('returns childManagerAccountID when all conditions are met for Send Money flow', async () => {
+    it('returns childManagerAccountID when all conditions are met for Send Money flow', () => {
         const {result} = renderHook(() =>
             useIDOfReportPreviewSender({
                 action: {...validAction, childMoneyRequestCount: 0},

From 389f7f933ad9f5d7150ea685a12d79deb998a622 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Mon, 30 Jun 2025 08:42:49 +0200
Subject: [PATCH 09/18] Correct comment in TransactionPreview/types

---
 src/components/ReportActionItem/TransactionPreview/types.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/ReportActionItem/TransactionPreview/types.ts b/src/components/ReportActionItem/TransactionPreview/types.ts
index 4a73fcd3a154..e485d9fc80bf 100644
--- a/src/components/ReportActionItem/TransactionPreview/types.ts
+++ b/src/components/ReportActionItem/TransactionPreview/types.ts
@@ -102,7 +102,7 @@ type TransactionPreviewContentProps = {
     /** Holds the transaction data entry from Onyx */
     transaction: OnyxEntry<Transaction>;
 
-    /** The original amount value on the transaction. This is used to deduce who is the sender and who is the receiver of the money request
+    /** The amount of the transaction saved in the database. This is used to deduce who is the sender and who is the receiver of the money request
      * In case of Splits the property `transaction` is actually an original transaction (for the whole split) and it does not have the data required to deduce who is the sender */
     transactionRawAmount: number;
 

From e338d966848ad498d49ebcfb46ceba325181b869 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Tue, 1 Jul 2025 11:22:13 +0200
Subject: [PATCH 10/18] Fix avatar for splits

---
 .../home/report/ReportActionItemSingle.tsx    | 48 ++++++++++++++-----
 1 file changed, 36 insertions(+), 12 deletions(-)

diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index 66049afd576d..6ea0fc4be319 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -41,7 +41,7 @@ import {
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
-import type {Policy, Report, ReportAction} from '@src/types/onyx';
+import type {Policy, Report, ReportAction, Transaction} from '@src/types/onyx';
 import type {Icon} from '@src/types/onyx/OnyxCommon';
 import type ChildrenProps from '@src/types/utils/ChildrenProps';
 import ReportActionItemDate from './ReportActionItemDate';
@@ -90,6 +90,22 @@ const showWorkspaceDetails = (reportID: string | undefined) => {
     Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID, Navigation.getReportRHPActiveRoute()));
 };
 
+function getSplitAuthor(transaction: Transaction, splits?: Array<ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU>>) {
+    const {originalTransactionID, source} = transaction.comment ?? {};
+
+    if (source !== CONST.IOU.TYPE.SPLIT || originalTransactionID === undefined) {
+        return undefined;
+    }
+
+    const splitAction = splits?.find((split) => getOriginalMessage(split)?.IOUTransactionID === originalTransactionID);
+
+    if (!splitAction) {
+        return undefined;
+    }
+
+    return splitAction.actorAccountID;
+}
+
 /**
  * This hook is used to determine the ID of the sender for the report preview action.
  * There are few fallbacks to determine the sender ID.
@@ -99,7 +115,7 @@ const showWorkspaceDetails = (reportID: string | undefined) => {
  *
  * For a reason why it is here, see https://github.com/Expensify/App/pull/64802 discussion
  */
-function useIDOfReportPreviewSender({action, iouReport}: {action: OnyxEntry<ReportAction>; iouReport?: OnyxEntry<Report>}) {
+function useIDOfReportPreviewSender({action, iouReport, report}: {action: OnyxEntry<ReportAction>; iouReport: OnyxEntry<Report>; report: OnyxEntry<Report>}) {
     const [iouActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, {
         canBeMissing: true,
         selector: (actions) => Object.values(actions ?? {}).filter(isMoneyRequestAction),
@@ -110,6 +126,14 @@ function useIDOfReportPreviewSender({action, iouReport}: {action: OnyxEntry<Repo
         selector: (allTransactions) => selectAllTransactionsForReport(allTransactions, action?.childReportID, iouActions ?? []),
     });
 
+    const [splits] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`, {
+        canBeMissing: true,
+        selector: (actions) =>
+            Object.values(actions ?? {})
+                .filter(isMoneyRequestAction)
+                .filter((act) => getOriginalMessage(act)?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT),
+    });
+
     if (action?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) {
         return;
     }
@@ -125,17 +149,17 @@ function useIDOfReportPreviewSender({action, iouReport}: {action: OnyxEntry<Repo
     // See: https://github.com/Expensify/App/pull/64802#issuecomment-3008944401
     const areAmountsSignsTheSame = new Set(transactions?.map((tr) => Math.sign(tr.amount))).size < 2;
 
-    // 3. If there is only one attendee - we check that by counting unique emails in the attendees list.
+    // 3. If there is only one attendee - we check that by counting unique emails converted to account IDs in the attendees list.
     // This has to be done as there are cases when transaction amounts signs are the same until report is opened.
     // See: https://github.com/Expensify/App/pull/64802#issuecomment-3007906310
-    const isThereOnlyOneAttendee =
-        new Set(
-            transactions
-                // If the transaction is a split, then it had to be created by someone even though the attendees are not present.
-                ?.flatMap<string | boolean>((tr) => tr.comment?.attendees?.map((att) => att.email) ?? tr.comment?.source === 'split')
-                // We filter out the concierge email and empty emails.
-                .filter((email) => !!email && email !== CONST.EMAIL.CONCIERGE),
-        ).size <= 1;
+
+    const attendeesIDs = transactions
+        // If the transaction is a split, then attendees are not present so we need to use a helper function.
+        ?.flatMap<number | undefined>((tr) => tr.comment?.attendees?.map((att) => getPersonalDetailByEmail(att.email)?.accountID) ?? getSplitAuthor(tr, splits))
+        // We filter out empty ID's
+        .filter((accountID) => !!accountID);
+
+    const isThereOnlyOneAttendee = new Set(attendeesIDs).size <= 1;
 
     // If the action is a 'Send Money' flow, it will only have one transaction, but the person who sent the money is the child manager account, not the child owner account.
     const isSendMoneyFlow = action?.childMoneyRequestCount === 0 && transactions?.length === 1;
@@ -177,7 +201,7 @@ function ReportActionItemSingle({
     const ownerAccountID = iouReport?.ownerAccountID ?? action?.childOwnerAccountID;
     const isReportPreviewAction = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW;
     const actorAccountID = getReportActionActorAccountID(action, iouReport, report, delegatePersonalDetails);
-    const reportPreviewSenderID = useIDOfReportPreviewSender({action, iouReport});
+    const reportPreviewSenderID = useIDOfReportPreviewSender({action, iouReport, report});
     const accountID = reportPreviewSenderID ?? actorAccountID ?? CONST.DEFAULT_NUMBER_ID;
 
     const invoiceReceiverPolicy =

From 3bd76cb111140f3d0ce355da3a55ad4e9390f582 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Tue, 1 Jul 2025 11:37:11 +0200
Subject: [PATCH 11/18] Remove avatar type check based on actions

---
 src/pages/home/report/ReportActionItemSingle.tsx | 15 ++++-----------
 1 file changed, 4 insertions(+), 11 deletions(-)

diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index 6ea0fc4be319..8bffcc50c9a9 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -30,7 +30,6 @@ import {
     getPolicyName,
     getReportActionActorAccountID,
     getWorkspaceIcon,
-    isActionCreator,
     isIndividualInvoiceRoom,
     isInvoiceReport as isInvoiceReportUtils,
     isInvoiceRoom,
@@ -140,16 +139,13 @@ function useIDOfReportPreviewSender({action, iouReport, report}: {action: OnyxEn
 
     /* We need to perform the following checks to determine if the report preview is a single avatar: */
 
-    // 1. If all actions are created by one person - either all actions are created by the current user or all actions are created by the user we have opened chat with.
-    // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-    const areActionsCreatedByOnePerson = iouActions?.every(isActionCreator) || !iouActions?.some(isActionCreator);
-
-    // 2. If all amounts have the same sign - either all amounts are positive or all amounts are negative.
+    // 1. If all amounts have the same sign - either all amounts are positive or all amounts are negative.
     // We have to do this because there can be a case when actions are not available
     // See: https://github.com/Expensify/App/pull/64802#issuecomment-3008944401
+
     const areAmountsSignsTheSame = new Set(transactions?.map((tr) => Math.sign(tr.amount))).size < 2;
 
-    // 3. If there is only one attendee - we check that by counting unique emails converted to account IDs in the attendees list.
+    // 2. If there is only one attendee - we check that by counting unique emails converted to account IDs in the attendees list.
     // This has to be done as there are cases when transaction amounts signs are the same until report is opened.
     // See: https://github.com/Expensify/App/pull/64802#issuecomment-3007906310
 
@@ -165,10 +161,7 @@ function useIDOfReportPreviewSender({action, iouReport, report}: {action: OnyxEn
     const isSendMoneyFlow = action?.childMoneyRequestCount === 0 && transactions?.length === 1;
     const singleAvatarAccountID = isSendMoneyFlow ? action.childManagerAccountID : action?.childOwnerAccountID;
 
-    // If we have more than one IOU action, but they are 'Pay' actions, we consider it as a single avatar. The only place where 'Pay' actions are considered valid is in the 'Send Money' flow.
-    const areThereValidIOUActions = !!iouActions?.length && (isSendMoneyFlow || iouActions.some((iouAction) => getOriginalMessage(iouAction)?.type !== CONST.IOU.REPORT_ACTION_TYPE.PAY));
-
-    return (!areThereValidIOUActions || areActionsCreatedByOnePerson) && areAmountsSignsTheSame && isThereOnlyOneAttendee ? singleAvatarAccountID : undefined;
+    return areAmountsSignsTheSame && isThereOnlyOneAttendee ? singleAvatarAccountID : undefined;
 }
 
 function ReportActionItemSingle({

From 110d83d74a6df42a842c7126c6966483e801c07e Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Tue, 1 Jul 2025 12:31:44 +0200
Subject: [PATCH 12/18] Fix Shawn issue & failing test

---
 __mocks__/reportData/personalDetails.ts       |  4 ++--
 src/libs/ReportUtils.ts                       |  2 +-
 .../home/report/ReportActionItemSingle.tsx    |  6 ++++--
 tests/unit/ReportActionItemSingleTest.ts      | 21 ++++++++++++++++---
 4 files changed, 25 insertions(+), 8 deletions(-)

diff --git a/__mocks__/reportData/personalDetails.ts b/__mocks__/reportData/personalDetails.ts
index b3c6179497f6..3d22e14b23b4 100644
--- a/__mocks__/reportData/personalDetails.ts
+++ b/__mocks__/reportData/personalDetails.ts
@@ -2,7 +2,7 @@ import type {PersonalDetailsList} from '@src/types/onyx';
 
 const usersIDs = [15593135, 51760358, 26502375] as const;
 
-const personalDetails: PersonalDetailsList = {
+const personalDetails = {
     [usersIDs[0]]: {
         accountID: usersIDs[0],
         avatar: '@assets/images/avatars/user/default-avatar_1.svg',
@@ -63,6 +63,6 @@ const personalDetails: PersonalDetailsList = {
         phoneNumber: '33333333',
         validated: true,
     },
-};
+} satisfies PersonalDetailsList;
 
 export default personalDetails;
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index ecf4c9e50936..f9a4b8643de7 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -8809,7 +8809,7 @@ function shouldReportShowSubscript(report: OnyxEntry<Report>): boolean {
         return true;
     }
 
-    if (isExpenseReport(report) && isOneTransactionReport(report)) {
+    if (isExpenseReport(report)) {
         return true;
     }
 
diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index 8bffcc50c9a9..31681e6a152a 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -151,7 +151,9 @@ function useIDOfReportPreviewSender({action, iouReport, report}: {action: OnyxEn
 
     const attendeesIDs = transactions
         // If the transaction is a split, then attendees are not present so we need to use a helper function.
-        ?.flatMap<number | undefined>((tr) => tr.comment?.attendees?.map((att) => getPersonalDetailByEmail(att.email)?.accountID) ?? getSplitAuthor(tr, splits))
+        ?.flatMap<number | undefined>((tr) =>
+            tr.comment?.attendees?.map((att) => (tr.comment?.source === CONST.IOU.TYPE.SPLIT ? getSplitAuthor(tr, splits) : getPersonalDetailByEmail(att.email)?.accountID)),
+        )
         // We filter out empty ID's
         .filter((accountID) => !!accountID);
 
@@ -201,7 +203,7 @@ function ReportActionItemSingle({
         report?.invoiceReceiver && 'policyID' in report.invoiceReceiver ? activePolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.invoiceReceiver.policyID}`] : undefined;
 
     let displayName = getDisplayNameForParticipant({accountID, personalDetailsData: personalDetails});
-    const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails?.[accountID ?? CONST.DEFAULT_NUMBER_ID] ?? {};
+    const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails?.[accountID] ?? {};
     const accountOwnerDetails = getPersonalDetailByEmail(login ?? '');
 
     // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
diff --git a/tests/unit/ReportActionItemSingleTest.ts b/tests/unit/ReportActionItemSingleTest.ts
index cfe3b893b9a9..ad1f93e1aede 100644
--- a/tests/unit/ReportActionItemSingleTest.ts
+++ b/tests/unit/ReportActionItemSingleTest.ts
@@ -1,17 +1,21 @@
 import {renderHook, screen, waitFor} from '@testing-library/react-native';
 import Onyx from 'react-native-onyx';
 import CONST from '@src/CONST';
+import * as PersonalDetailsUtils from '@src/libs/PersonalDetailsUtils';
 import ONYXKEYS from '@src/ONYXKEYS';
 import {DO_NOT_USE__EXPORT_FOR_TESTS__useIDOfReportPreviewSender as useIDOfReportPreviewSender} from '@src/pages/home/report/ReportActionItemSingle';
 import type {PersonalDetailsList} from '@src/types/onyx';
 import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet';
 import {actionR14932, actionR98765} from '../../__mocks__/reportData/actions';
-import {iouReportR14932} from '../../__mocks__/reportData/reports';
+import personalDetails from '../../__mocks__/reportData/personalDetails';
+import {chatReportR14932, iouReportR14932} from '../../__mocks__/reportData/reports';
 import {transactionR14932} from '../../__mocks__/reportData/transactions';
 import * as LHNTestUtils from '../utils/LHNTestUtils';
 import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
 import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates';
 
+import PropertyKeysOf = jest.PropertyKeysOf;
+
 describe('ReportActionItemSingle', () => {
     beforeAll(() =>
         Onyx.init({
@@ -100,10 +104,16 @@ const validAction = {
 };
 
 describe('useIDOfReportPreviewSender', () => {
+    const mockedEmailToID: Record<string, PropertyKeysOf<typeof personalDetails>> = {
+        [personalDetails[15593135].login]: 15593135,
+        [personalDetails[51760358].login]: 51760358,
+    };
+
     beforeAll(() => {
         Onyx.init({
             keys: ONYXKEYS,
         });
+        jest.spyOn(PersonalDetailsUtils, 'getPersonalDetailByEmail').mockImplementation((email) => personalDetails[mockedEmailToID[email]]);
     });
 
     beforeEach(() => {
@@ -124,6 +134,7 @@ describe('useIDOfReportPreviewSender', () => {
             useIDOfReportPreviewSender({
                 action: actionR14932,
                 iouReport: iouReportR14932,
+                report: chatReportR14932,
             }),
         );
         expect(result.current).toBeUndefined();
@@ -134,6 +145,7 @@ describe('useIDOfReportPreviewSender', () => {
             useIDOfReportPreviewSender({
                 action: {...validAction, childMoneyRequestCount: 0},
                 iouReport: iouReportR14932,
+                report: chatReportR14932,
             }),
         );
         expect(result.current).toBe(iouReportR14932.managerID);
@@ -143,19 +155,20 @@ describe('useIDOfReportPreviewSender', () => {
         await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}`, {
             ...transactionR14932,
             comment: {
-                attendees: [{email: 'test@test.com', displayName: 'Test One', avatarUrl: 'https://none.com/none'}],
+                attendees: [{email: personalDetails[15593135].login, displayName: 'Test One', avatarUrl: 'https://none.com/none'}],
             },
         });
         await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}2`, {
             ...transactionR14932,
             comment: {
-                attendees: [{email: 'test2@test.com', displayName: 'Test Two', avatarUrl: 'https://none.com/none2'}],
+                attendees: [{email: personalDetails[51760358].login, displayName: 'Test Two', avatarUrl: 'https://none.com/none2'}],
             },
         });
         const {result} = renderHook(() =>
             useIDOfReportPreviewSender({
                 action: validAction,
                 iouReport: iouReportR14932,
+                report: chatReportR14932,
             }),
         );
         expect(result.current).toBeUndefined();
@@ -174,6 +187,7 @@ describe('useIDOfReportPreviewSender', () => {
             useIDOfReportPreviewSender({
                 action: validAction,
                 iouReport: iouReportR14932,
+                report: chatReportR14932,
             }),
         );
         expect(result.current).toBeUndefined();
@@ -184,6 +198,7 @@ describe('useIDOfReportPreviewSender', () => {
             useIDOfReportPreviewSender({
                 action: validAction,
                 iouReport: iouReportR14932,
+                report: chatReportR14932,
             }),
         );
         expect(result.current).toBe(iouReportR14932.ownerAccountID);

From 2df473d463d4823514635df8608eeff852f7daf3 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Mon, 7 Jul 2025 08:17:35 +0200
Subject: [PATCH 13/18] Extract useReportPreviewSenderID

---
 .../home/report/ReportActionItemSingle.tsx    | 86 +-----------------
 .../home/report/useReportPreviewSenderID.tsx  | 87 +++++++++++++++++++
 tests/unit/ReportActionItemSingleTest.ts      | 14 +--
 3 files changed, 98 insertions(+), 89 deletions(-)
 create mode 100644 src/pages/home/report/useReportPreviewSenderID.tsx

diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index 31681e6a152a..06db2a541f64 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -19,10 +19,9 @@ import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import ControlSelection from '@libs/ControlSelection';
 import DateUtils from '@libs/DateUtils';
-import {selectAllTransactionsForReport} from '@libs/MoneyRequestReportUtils';
 import Navigation from '@libs/Navigation/Navigation';
 import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
-import {getOriginalMessage, getReportActionMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
+import {getReportActionMessage} from '@libs/ReportActionsUtils';
 import {
     getDefaultWorkspaceAvatar,
     getDisplayNameForParticipant,
@@ -40,11 +39,12 @@ import {
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
-import type {Policy, Report, ReportAction, Transaction} from '@src/types/onyx';
+import type {Policy, Report, ReportAction} from '@src/types/onyx';
 import type {Icon} from '@src/types/onyx/OnyxCommon';
 import type ChildrenProps from '@src/types/utils/ChildrenProps';
 import ReportActionItemDate from './ReportActionItemDate';
 import ReportActionItemFragment from './ReportActionItemFragment';
+import useReportPreviewSenderID from './useReportPreviewSenderID';
 
 type ReportActionItemSingleProps = Partial<ChildrenProps> & {
     /** All the data of the action */
@@ -89,83 +89,6 @@ const showWorkspaceDetails = (reportID: string | undefined) => {
     Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(reportID, Navigation.getReportRHPActiveRoute()));
 };
 
-function getSplitAuthor(transaction: Transaction, splits?: Array<ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU>>) {
-    const {originalTransactionID, source} = transaction.comment ?? {};
-
-    if (source !== CONST.IOU.TYPE.SPLIT || originalTransactionID === undefined) {
-        return undefined;
-    }
-
-    const splitAction = splits?.find((split) => getOriginalMessage(split)?.IOUTransactionID === originalTransactionID);
-
-    if (!splitAction) {
-        return undefined;
-    }
-
-    return splitAction.actorAccountID;
-}
-
-/**
- * This hook is used to determine the ID of the sender for the report preview action.
- * There are few fallbacks to determine the sender ID.
- *
- * It should not be used outside of this component.
- * The only reason it is exported to a hook is to make it easier to remove it in the future.
- *
- * For a reason why it is here, see https://github.com/Expensify/App/pull/64802 discussion
- */
-function useIDOfReportPreviewSender({action, iouReport, report}: {action: OnyxEntry<ReportAction>; iouReport: OnyxEntry<Report>; report: OnyxEntry<Report>}) {
-    const [iouActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, {
-        canBeMissing: true,
-        selector: (actions) => Object.values(actions ?? {}).filter(isMoneyRequestAction),
-    });
-
-    const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
-        canBeMissing: true,
-        selector: (allTransactions) => selectAllTransactionsForReport(allTransactions, action?.childReportID, iouActions ?? []),
-    });
-
-    const [splits] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`, {
-        canBeMissing: true,
-        selector: (actions) =>
-            Object.values(actions ?? {})
-                .filter(isMoneyRequestAction)
-                .filter((act) => getOriginalMessage(act)?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT),
-    });
-
-    if (action?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) {
-        return;
-    }
-
-    /* We need to perform the following checks to determine if the report preview is a single avatar: */
-
-    // 1. If all amounts have the same sign - either all amounts are positive or all amounts are negative.
-    // We have to do this because there can be a case when actions are not available
-    // See: https://github.com/Expensify/App/pull/64802#issuecomment-3008944401
-
-    const areAmountsSignsTheSame = new Set(transactions?.map((tr) => Math.sign(tr.amount))).size < 2;
-
-    // 2. If there is only one attendee - we check that by counting unique emails converted to account IDs in the attendees list.
-    // This has to be done as there are cases when transaction amounts signs are the same until report is opened.
-    // See: https://github.com/Expensify/App/pull/64802#issuecomment-3007906310
-
-    const attendeesIDs = transactions
-        // If the transaction is a split, then attendees are not present so we need to use a helper function.
-        ?.flatMap<number | undefined>((tr) =>
-            tr.comment?.attendees?.map((att) => (tr.comment?.source === CONST.IOU.TYPE.SPLIT ? getSplitAuthor(tr, splits) : getPersonalDetailByEmail(att.email)?.accountID)),
-        )
-        // We filter out empty ID's
-        .filter((accountID) => !!accountID);
-
-    const isThereOnlyOneAttendee = new Set(attendeesIDs).size <= 1;
-
-    // If the action is a 'Send Money' flow, it will only have one transaction, but the person who sent the money is the child manager account, not the child owner account.
-    const isSendMoneyFlow = action?.childMoneyRequestCount === 0 && transactions?.length === 1;
-    const singleAvatarAccountID = isSendMoneyFlow ? action.childManagerAccountID : action?.childOwnerAccountID;
-
-    return areAmountsSignsTheSame && isThereOnlyOneAttendee ? singleAvatarAccountID : undefined;
-}
-
 function ReportActionItemSingle({
     action,
     children,
@@ -196,7 +119,7 @@ function ReportActionItemSingle({
     const ownerAccountID = iouReport?.ownerAccountID ?? action?.childOwnerAccountID;
     const isReportPreviewAction = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW;
     const actorAccountID = getReportActionActorAccountID(action, iouReport, report, delegatePersonalDetails);
-    const reportPreviewSenderID = useIDOfReportPreviewSender({action, iouReport, report});
+    const reportPreviewSenderID = useReportPreviewSenderID({action, iouReport, report});
     const accountID = reportPreviewSenderID ?? actorAccountID ?? CONST.DEFAULT_NUMBER_ID;
 
     const invoiceReceiverPolicy =
@@ -441,4 +364,3 @@ function ReportActionItemSingle({
 ReportActionItemSingle.displayName = 'ReportActionItemSingle';
 
 export default ReportActionItemSingle;
-export {useIDOfReportPreviewSender as DO_NOT_USE__EXPORT_FOR_TESTS__useIDOfReportPreviewSender};
diff --git a/src/pages/home/report/useReportPreviewSenderID.tsx b/src/pages/home/report/useReportPreviewSenderID.tsx
new file mode 100644
index 000000000000..09778daa329e
--- /dev/null
+++ b/src/pages/home/report/useReportPreviewSenderID.tsx
@@ -0,0 +1,87 @@
+import type {OnyxEntry} from 'react-native-onyx';
+import useOnyx from '@hooks/useOnyx';
+import {selectAllTransactionsForReport} from '@libs/MoneyRequestReportUtils';
+import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
+import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Report, ReportAction, Transaction} from '@src/types/onyx';
+
+function getSplitAuthor(transaction: Transaction, splits?: Array<ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU>>) {
+    const {originalTransactionID, source} = transaction.comment ?? {};
+
+    if (source !== CONST.IOU.TYPE.SPLIT || originalTransactionID === undefined) {
+        return undefined;
+    }
+
+    const splitAction = splits?.find((split) => getOriginalMessage(split)?.IOUTransactionID === originalTransactionID);
+
+    if (!splitAction) {
+        return undefined;
+    }
+
+    return splitAction.actorAccountID;
+}
+
+/**
+ * This hook is used to determine the ID of the sender for the report preview action.
+ * There are few fallbacks to determine the sender ID.
+ *
+ * It should not be used outside of this component.
+ * The only reason it is exported to a hook is to make it easier to remove it in the future.
+ *
+ * For a reason why it is here, see https://github.com/Expensify/App/pull/64802 discussion
+ */
+function useReportPreviewSenderID({action, iouReport, report}: {action: OnyxEntry<ReportAction>; iouReport: OnyxEntry<Report>; report: OnyxEntry<Report>}) {
+    const [iouActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, {
+        canBeMissing: true,
+        selector: (actions) => Object.values(actions ?? {}).filter(isMoneyRequestAction),
+    });
+
+    const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
+        canBeMissing: true,
+        selector: (allTransactions) => selectAllTransactionsForReport(allTransactions, action?.childReportID, iouActions ?? []),
+    });
+
+    const [splits] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`, {
+        canBeMissing: true,
+        selector: (actions) =>
+            Object.values(actions ?? {})
+                .filter(isMoneyRequestAction)
+                .filter((act) => getOriginalMessage(act)?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT),
+    });
+
+    if (action?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) {
+        return;
+    }
+
+    /* We need to perform the following checks to determine if the report preview is a single avatar: */
+
+    // 1. If all amounts have the same sign - either all amounts are positive or all amounts are negative.
+    // We have to do this because there can be a case when actions are not available
+    // See: https://github.com/Expensify/App/pull/64802#issuecomment-3008944401
+
+    const areAmountsSignsTheSame = new Set(transactions?.map((tr) => Math.sign(tr.amount))).size < 2;
+
+    // 2. If there is only one attendee - we check that by counting unique emails converted to account IDs in the attendees list.
+    // This has to be done as there are cases when transaction amounts signs are the same until report is opened.
+    // See: https://github.com/Expensify/App/pull/64802#issuecomment-3007906310
+
+    const attendeesIDs = transactions
+        // If the transaction is a split, then attendees are not present so we need to use a helper function.
+        ?.flatMap<number | undefined>((tr) =>
+            tr.comment?.attendees?.map((att) => (tr.comment?.source === CONST.IOU.TYPE.SPLIT ? getSplitAuthor(tr, splits) : getPersonalDetailByEmail(att.email)?.accountID)),
+        )
+        // We filter out empty ID's
+        .filter((accountID) => !!accountID);
+
+    const isThereOnlyOneAttendee = new Set(attendeesIDs).size <= 1;
+
+    // If the action is a 'Send Money' flow, it will only have one transaction, but the person who sent the money is the child manager account, not the child owner account.
+    const isSendMoneyFlow = action?.childMoneyRequestCount === 0 && transactions?.length === 1;
+    const singleAvatarAccountID = isSendMoneyFlow ? action.childManagerAccountID : action?.childOwnerAccountID;
+
+    return areAmountsSignsTheSame && isThereOnlyOneAttendee ? singleAvatarAccountID : undefined;
+}
+
+export default useReportPreviewSenderID;
diff --git a/tests/unit/ReportActionItemSingleTest.ts b/tests/unit/ReportActionItemSingleTest.ts
index ad1f93e1aede..0eb98e4b740f 100644
--- a/tests/unit/ReportActionItemSingleTest.ts
+++ b/tests/unit/ReportActionItemSingleTest.ts
@@ -1,9 +1,9 @@
 import {renderHook, screen, waitFor} from '@testing-library/react-native';
 import Onyx from 'react-native-onyx';
+import useReportPreviewSenderID from '@pages/home/report/useReportPreviewSenderID';
 import CONST from '@src/CONST';
 import * as PersonalDetailsUtils from '@src/libs/PersonalDetailsUtils';
 import ONYXKEYS from '@src/ONYXKEYS';
-import {DO_NOT_USE__EXPORT_FOR_TESTS__useIDOfReportPreviewSender as useIDOfReportPreviewSender} from '@src/pages/home/report/ReportActionItemSingle';
 import type {PersonalDetailsList} from '@src/types/onyx';
 import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet';
 import {actionR14932, actionR98765} from '../../__mocks__/reportData/actions';
@@ -103,7 +103,7 @@ const validAction = {
     childManagerAccountID: iouReportR14932.managerID,
 };
 
-describe('useIDOfReportPreviewSender', () => {
+describe('useReportPreviewSenderID', () => {
     const mockedEmailToID: Record<string, PropertyKeysOf<typeof personalDetails>> = {
         [personalDetails[15593135].login]: 15593135,
         [personalDetails[51760358].login]: 51760358,
@@ -131,7 +131,7 @@ describe('useIDOfReportPreviewSender', () => {
 
     it('returns undefined when action is not a report preview', () => {
         const {result} = renderHook(() =>
-            useIDOfReportPreviewSender({
+            useReportPreviewSenderID({
                 action: actionR14932,
                 iouReport: iouReportR14932,
                 report: chatReportR14932,
@@ -142,7 +142,7 @@ describe('useIDOfReportPreviewSender', () => {
 
     it('returns childManagerAccountID when all conditions are met for Send Money flow', () => {
         const {result} = renderHook(() =>
-            useIDOfReportPreviewSender({
+            useReportPreviewSenderID({
                 action: {...validAction, childMoneyRequestCount: 0},
                 iouReport: iouReportR14932,
                 report: chatReportR14932,
@@ -165,7 +165,7 @@ describe('useIDOfReportPreviewSender', () => {
             },
         });
         const {result} = renderHook(() =>
-            useIDOfReportPreviewSender({
+            useReportPreviewSenderID({
                 action: validAction,
                 iouReport: iouReportR14932,
                 report: chatReportR14932,
@@ -184,7 +184,7 @@ describe('useIDOfReportPreviewSender', () => {
             amount: -100,
         });
         const {result} = renderHook(() =>
-            useIDOfReportPreviewSender({
+            useReportPreviewSenderID({
                 action: validAction,
                 iouReport: iouReportR14932,
                 report: chatReportR14932,
@@ -195,7 +195,7 @@ describe('useIDOfReportPreviewSender', () => {
 
     it('returns childOwnerAccountID when all conditions are met', () => {
         const {result} = renderHook(() =>
-            useIDOfReportPreviewSender({
+            useReportPreviewSenderID({
                 action: validAction,
                 iouReport: iouReportR14932,
                 report: chatReportR14932,

From 805494b1c29dd0b292fa53c696b44201d202e7b0 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Mon, 7 Jul 2025 13:15:30 +0200
Subject: [PATCH 14/18] Refractor, extract & adapt the logic for
 MoneyReportHeaders

---
 src/components/AvatarWithDisplayName.tsx      |  36 +-
 src/components/HeaderWithBackButton/index.tsx |   2 +
 src/components/HeaderWithBackButton/types.ts  |   4 +
 src/components/MoneyReportHeader.tsx          |  13 +
 .../home/report/ReportActionItemSingle.tsx    | 173 ++-------
 .../home/report/useReportPreviewDetails.tsx   | 327 ++++++++++++++++++
 .../home/report/useReportPreviewSenderID.tsx  |  87 -----
 tests/unit/ReportActionItemSingleTest.ts      |  41 ++-
 8 files changed, 440 insertions(+), 243 deletions(-)
 create mode 100644 src/pages/home/report/useReportPreviewDetails.tsx
 delete mode 100644 src/pages/home/report/useReportPreviewSenderID.tsx

diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx
index a0fb57fff240..1790af9d5d6a 100644
--- a/src/components/AvatarWithDisplayName.tsx
+++ b/src/components/AvatarWithDisplayName.tsx
@@ -27,6 +27,8 @@ import {
     navigateToDetailsPage,
     shouldReportShowSubscript,
 } from '@libs/ReportUtils';
+import type {AvatarDetails} from '@pages/home/report/useReportPreviewDetails';
+import {getReportPreviewSenderAvatar} from '@pages/home/report/useReportPreviewDetails';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
@@ -73,6 +75,9 @@ type AvatarWithDisplayNameProps = {
 
     /** Color of the secondary avatar border, usually should match the container background */
     avatarBorderColor?: ColorValue;
+
+    /** If we want to override the default avatar behavior and set a single avatar, we should pass this prop. */
+    singleAvatarDetails?: AvatarDetails;
 };
 
 const fallbackIcon: Icon = {
@@ -167,6 +172,7 @@ function AvatarWithDisplayName({
     shouldEnableAvatarNavigation = true,
     shouldUseCustomSearchTitleName = false,
     transactions = [],
+    singleAvatarDetails,
     openParentReportInCurrentTab = false,
     avatarBorderColor: avatarBorderColorProp,
 }: AvatarWithDisplayNameProps) {
@@ -236,25 +242,41 @@ function AvatarWithDisplayName({
             Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
         }
     }, [report, shouldEnableDetailPageNavigation, goToDetailsPage]);
+
     const shouldUseFullTitle = isMoneyRequestOrReport || isAnonymous;
-    const avatar = (
-        <View accessibilityLabel={title}>
-            {shouldShowSubscriptAvatar ? (
+
+    const getAvatar = () => {
+        if (shouldShowSubscriptAvatar) {
+            return (
                 <SubscriptAvatar
                     backgroundColor={avatarBorderColor}
                     mainAvatar={icons.at(0) ?? fallbackIcon}
                     secondaryAvatar={icons.at(1)}
                     size={size}
                 />
-            ) : (
+            );
+        }
+
+        if (!singleAvatarDetails || singleAvatarDetails.shouldDisplayAllActors || !singleAvatarDetails.reportPreviewSenderID) {
+            return (
                 <MultipleAvatars
                     icons={icons}
                     size={size}
                     secondAvatarStyle={[StyleUtils.getBackgroundAndBorderStyle(avatarBorderColor)]}
                 />
-            )}
-        </View>
-    );
+            );
+        }
+
+        return getReportPreviewSenderAvatar({
+            reportPreviewDetails: singleAvatarDetails,
+            personalDetails,
+            containerStyles: [styles.actionAvatar, styles.mr3],
+            actorAccountID: actorAccountID?.current,
+        });
+    };
+
+    const avatar = <View accessibilityLabel={title}>{getAvatar()}</View>;
+
     const headerView = (
         <View style={[styles.appContentHeaderTitle, styles.flex1]}>
             {!!report && !!title && (
diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx
index 7bf69ec39b39..1d358f9ebddd 100755
--- a/src/components/HeaderWithBackButton/index.tsx
+++ b/src/components/HeaderWithBackButton/index.tsx
@@ -37,6 +37,7 @@ function HeaderWithBackButton({
     report,
     policy,
     policyAvatar,
+    singleAvatarDetails,
     shouldShowReportAvatarWithDisplay = false,
     shouldShowBackButton = true,
     shouldShowBorderBottom = false,
@@ -103,6 +104,7 @@ function HeaderWithBackButton({
                 <AvatarWithDisplayName
                     report={report}
                     policy={policy}
+                    singleAvatarDetails={singleAvatarDetails}
                     shouldEnableDetailPageNavigation={shouldEnableDetailPageNavigation}
                     openParentReportInCurrentTab={openParentReportInCurrentTab}
                 />
diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts
index 19135d36ace5..f742884baf4f 100644
--- a/src/components/HeaderWithBackButton/types.ts
+++ b/src/components/HeaderWithBackButton/types.ts
@@ -3,6 +3,7 @@ import type {StyleProp, ViewStyle} from 'react-native';
 import type {OnyxEntry} from 'react-native-onyx';
 import type {PopoverMenuItem} from '@components/PopoverMenu';
 import type {Action} from '@hooks/useSingleExecution';
+import type {AvatarDetails} from '@pages/home/report/useReportPreviewDetails';
 import type {StepCounterParams} from '@src/languages/params';
 import type {TranslationPaths} from '@src/languages/types';
 import type {AnchorPosition} from '@src/styles';
@@ -161,6 +162,9 @@ type HeaderWithBackButtonProps = Partial<ChildrenProps> & {
     shouldMinimizeMenuButton?: boolean;
     /** Whether to open the parent report link in the current tab if possible */
     openParentReportInCurrentTab?: boolean;
+
+    /** If we want to override the default avatar behavior and set a single avatar, we should pass this prop. */
+    singleAvatarDetails?: AvatarDetails;
 };
 
 export type {ThreeDotsMenuItem};
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 35f1fc35434b..261dcf3143ff 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -60,6 +60,7 @@ import {
     shouldShowBrokenConnectionViolationForMultipleTransactions,
 } from '@libs/TransactionUtils';
 import type {ExportType} from '@pages/home/report/ReportDetailsExportPage';
+import useReportPreviewDetails from '@pages/home/report/useReportPreviewDetails';
 import variables from '@styles/variables';
 import {
     approveMoneyRequest,
@@ -185,6 +186,15 @@ function MoneyReportHeader({
     const isExported = isExportedUtils(reportActions);
     const integrationNameFromExportMessage = isExported ? getIntegrationNameFromExportMessageUtils(reportActions) : null;
 
+    const [reportPreviewAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, {
+        canBeMissing: true,
+        selector: (actions) => Object.entries(actions ?? {}).find(([id]) => id === moneyRequestReport?.parentReportActionID)?.[1],
+    });
+
+    const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {
+        canBeMissing: true,
+    });
+
     const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false);
     const [isCancelPaymentModalVisible, setIsCancelPaymentModalVisible] = useState(false);
     const [isDeleteExpenseModalVisible, setIsDeleteExpenseModalVisible] = useState(false);
@@ -220,6 +230,8 @@ function MoneyReportHeader({
         [allViolations, transactionIDs],
     );
 
+    const details = useReportPreviewDetails({report: chatReport, iouReport: moneyRequestReport, action: reportPreviewAction, policy, innerPolicies: policies, personalDetails});
+
     const messagePDF = useMemo(() => {
         if (!reportPDFFilename) {
             return translate('reportDetailsPage.waitForPDF');
@@ -944,6 +956,7 @@ function MoneyReportHeader({
             <HeaderWithBackButton
                 shouldShowReportAvatarWithDisplay
                 shouldShowPinButton={false}
+                singleAvatarDetails={details}
                 report={moneyRequestReport}
                 policy={policy}
                 shouldShowBackButton={shouldShowBackButton}
diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index 06db2a541f64..f1551770c46c 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -2,15 +2,12 @@ import React, {useCallback, useMemo} from 'react';
 import type {StyleProp, ViewStyle} from 'react-native';
 import {View} from 'react-native';
 import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
-import Avatar from '@components/Avatar';
-import {FallbackAvatar} from '@components/Icon/Expensicons';
 import MultipleAvatars from '@components/MultipleAvatars';
 import OfflineWithFeedback from '@components/OfflineWithFeedback';
 import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
 import SubscriptAvatar from '@components/SubscriptAvatar';
 import Text from '@components/Text';
 import Tooltip from '@components/Tooltip';
-import UserDetailsTooltip from '@components/UserDetailsTooltip';
 import useLocalize from '@hooks/useLocalize';
 import useOnyx from '@hooks/useOnyx';
 import usePolicy from '@hooks/usePolicy';
@@ -22,29 +19,15 @@ import DateUtils from '@libs/DateUtils';
 import Navigation from '@libs/Navigation/Navigation';
 import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
 import {getReportActionMessage} from '@libs/ReportActionsUtils';
-import {
-    getDefaultWorkspaceAvatar,
-    getDisplayNameForParticipant,
-    getIcons,
-    getPolicyName,
-    getReportActionActorAccountID,
-    getWorkspaceIcon,
-    isIndividualInvoiceRoom,
-    isInvoiceReport as isInvoiceReportUtils,
-    isInvoiceRoom,
-    isOptimisticPersonalDetail,
-    isPolicyExpenseChat,
-    isTripRoom as isTripRoomReportUtils,
-} from '@libs/ReportUtils';
+import {getReportActionActorAccountID, isOptimisticPersonalDetail} from '@libs/ReportUtils';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
 import type {Policy, Report, ReportAction} from '@src/types/onyx';
-import type {Icon} from '@src/types/onyx/OnyxCommon';
 import type ChildrenProps from '@src/types/utils/ChildrenProps';
 import ReportActionItemDate from './ReportActionItemDate';
 import ReportActionItemFragment from './ReportActionItemFragment';
-import useReportPreviewSenderID from './useReportPreviewSenderID';
+import useReportPreviewDetails, {getReportPreviewSenderAvatar} from './useReportPreviewDetails';
 
 type ReportActionItemSingleProps = Partial<ChildrenProps> & {
     /** All the data of the action */
@@ -109,108 +92,34 @@ function ReportActionItemSingle({
     const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {
         canBeMissing: true,
     });
+
     const [innerPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {
         canBeMissing: true,
     });
 
-    const activePolicies = policies ?? innerPolicies;
     const policy = usePolicy(report?.policyID);
+
     const delegatePersonalDetails = action?.delegateAccountID ? personalDetails?.[action?.delegateAccountID] : undefined;
-    const ownerAccountID = iouReport?.ownerAccountID ?? action?.childOwnerAccountID;
-    const isReportPreviewAction = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW;
     const actorAccountID = getReportActionActorAccountID(action, iouReport, report, delegatePersonalDetails);
-    const reportPreviewSenderID = useReportPreviewSenderID({action, iouReport, report});
-    const accountID = reportPreviewSenderID ?? actorAccountID ?? CONST.DEFAULT_NUMBER_ID;
-
-    const invoiceReceiverPolicy =
-        report?.invoiceReceiver && 'policyID' in report.invoiceReceiver ? activePolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.invoiceReceiver.policyID}`] : undefined;
-
-    let displayName = getDisplayNameForParticipant({accountID, personalDetailsData: personalDetails});
-    const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails?.[accountID] ?? {};
-    const accountOwnerDetails = getPersonalDetailByEmail(login ?? '');
 
-    // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-    let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '');
-    const isTripRoom = isTripRoomReportUtils(report);
-
-    // We want to display only the sender's avatar next to the report preview if it only contains one person's expenses.
-    const displayAllActors = isReportPreviewAction && !isTripRoom && !isPolicyExpenseChat(report) && !reportPreviewSenderID;
-    const isInvoiceReport = isInvoiceReportUtils(iouReport ?? null);
-    const isWorkspaceActor = isInvoiceReport || (isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors));
-
-    let avatarSource = avatar;
-    let avatarId: number | string | undefined = actorAccountID;
-    if (isWorkspaceActor) {
-        displayName = getPolicyName({report, policy});
-        actorHint = displayName;
-        avatarSource = getWorkspaceIcon(report, policy).source;
-        avatarId = report?.policyID;
-    } else if (delegatePersonalDetails) {
-        displayName = delegatePersonalDetails?.displayName ?? '';
-        avatarSource = delegatePersonalDetails?.avatar;
-        avatarId = delegatePersonalDetails?.accountID;
-    } else if (isReportPreviewAction && isTripRoom) {
-        displayName = report?.reportName ?? '';
-        avatarSource = personalDetails?.[ownerAccountID ?? CONST.DEFAULT_NUMBER_ID]?.avatar;
-        avatarId = ownerAccountID;
-    }
-
-    // If this is a report preview, display names and avatars of both people involved
-    let secondaryAvatar: Icon;
-    const primaryDisplayName = displayName;
-    if (displayAllActors) {
-        if (isInvoiceRoom(report) && !isIndividualInvoiceRoom(report)) {
-            const secondaryPolicyAvatar = invoiceReceiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(invoiceReceiverPolicy?.name);
-
-            secondaryAvatar = {
-                source: secondaryPolicyAvatar,
-                type: CONST.ICON_TYPE_WORKSPACE,
-                name: invoiceReceiverPolicy?.name,
-                id: invoiceReceiverPolicy?.id,
-            };
-        } else {
-            // The ownerAccountID and actorAccountID can be the same if a user submits an expense back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice
-            const secondaryAccountId = ownerAccountID === actorAccountID || isInvoiceReport ? actorAccountID : ownerAccountID;
-            const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar;
-            const secondaryDisplayName = getDisplayNameForParticipant({accountID: secondaryAccountId});
-
-            secondaryAvatar = {
-                source: secondaryUserAvatar,
-                type: CONST.ICON_TYPE_AVATAR,
-                name: secondaryDisplayName ?? '',
-                id: secondaryAccountId,
-            };
-        }
-    } else if (!isWorkspaceActor) {
-        // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
-        const avatarIconIndex = report?.isOwnPolicyExpenseChat || isPolicyExpenseChat(report) ? 0 : 1;
-        const reportIcons = getIcons(report, personalDetails, undefined, undefined, undefined, policy);
-
-        secondaryAvatar = reportIcons.at(avatarIconIndex) ?? {name: '', source: '', type: CONST.ICON_TYPE_AVATAR};
-    } else if (isInvoiceReportUtils(iouReport)) {
-        const secondaryAccountId = iouReport?.managerID ?? CONST.DEFAULT_NUMBER_ID;
-        const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar;
-        const secondaryDisplayName = getDisplayNameForParticipant({accountID: secondaryAccountId});
+    const reportPreviewDetails = useReportPreviewDetails({
+        action,
+        report,
+        iouReport,
+        policies,
+        personalDetails,
+        innerPolicies,
+        policy,
+    });
 
-        secondaryAvatar = {
-            source: secondaryUserAvatar,
-            type: CONST.ICON_TYPE_AVATAR,
-            name: secondaryDisplayName,
-            id: secondaryAccountId,
-        };
-    } else {
-        secondaryAvatar = {name: '', source: '', type: 'avatar'};
-    }
-    const icon = {
-        source: avatarSource ?? FallbackAvatar,
-        type: isWorkspaceActor ? CONST.ICON_TYPE_WORKSPACE : CONST.ICON_TYPE_AVATAR,
-        name: primaryDisplayName ?? '',
-        id: avatarId,
-    };
+    const {primaryAvatar, secondaryAvatar, displayName, shouldDisplayAllActors, isWorkspaceActor, reportPreviewSenderID, actorHint} = reportPreviewDetails;
+    const accountID = reportPreviewSenderID ?? actorAccountID ?? CONST.DEFAULT_NUMBER_ID;
 
-    const showMultipleUserAvatarPattern = displayAllActors && !shouldShowSubscriptAvatar;
+    const {login, pendingFields, status} = personalDetails?.[accountID] ?? {};
+    const accountOwnerDetails = getPersonalDetailByEmail(login ?? '');
 
-    const headingText = showMultipleUserAvatarPattern ? `${icon.name} & ${secondaryAvatar.name}` : displayName;
+    const showMultipleUserAvatarPattern = shouldDisplayAllActors && !shouldShowSubscriptAvatar;
+    const headingText = showMultipleUserAvatarPattern ? `${primaryAvatar.name} & ${secondaryAvatar.name}` : displayName;
 
     // Since the display name for a report action message is delivered with the report history as an array of fragments
     // we'll need to take the displayName from personal details and have it be in the same format for now. Eventually,
@@ -232,13 +141,13 @@ function ReportActionItemSingle({
             showWorkspaceDetails(reportID);
         } else {
             // Show participants page IOU report preview
-            if (iouReportID && displayAllActors) {
+            if (iouReportID && shouldDisplayAllActors) {
                 Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(iouReportID, Navigation.getReportRHPActiveRoute()));
                 return;
             }
-            showUserDetails(Number(icon.id));
+            showUserDetails(Number(primaryAvatar.id));
         }
-    }, [isWorkspaceActor, reportID, iouReportID, displayAllActors, icon.id]);
+    }, [isWorkspaceActor, reportID, iouReportID, shouldDisplayAllActors, primaryAvatar.id]);
 
     const shouldDisableDetailPage = useMemo(
         () =>
@@ -256,48 +165,38 @@ function ReportActionItemSingle({
         }
         return theme.sidebar;
     };
+
     const getAvatar = () => {
         if (shouldShowSubscriptAvatar) {
             return (
                 <SubscriptAvatar
-                    mainAvatar={icon}
+                    mainAvatar={primaryAvatar}
                     secondaryAvatar={secondaryAvatar}
                     noMargin
                     backgroundColor={getBackgroundColor()}
                 />
             );
         }
-        if (displayAllActors) {
+        if (shouldDisplayAllActors) {
             return (
                 <MultipleAvatars
-                    icons={[icon, secondaryAvatar]}
+                    icons={[primaryAvatar, secondaryAvatar]}
                     isInReportAction
                     shouldShowTooltip
                     secondAvatarStyle={[StyleUtils.getBackgroundAndBorderStyle(theme.appBG), isHovered ? StyleUtils.getBackgroundAndBorderStyle(theme.hoverComponentBG) : undefined]}
                 />
             );
         }
-        return (
-            <UserDetailsTooltip
-                accountID={Number(delegatePersonalDetails && !isWorkspaceActor ? actorAccountID : (icon.id ?? CONST.DEFAULT_NUMBER_ID))}
-                delegateAccountID={action?.delegateAccountID}
-                icon={icon}
-            >
-                <View>
-                    <Avatar
-                        containerStyles={[styles.actionAvatar]}
-                        source={icon.source}
-                        type={icon.type}
-                        name={icon.name}
-                        avatarID={icon.id}
-                        fallbackIcon={fallbackIcon}
-                    />
-                </View>
-            </UserDetailsTooltip>
-        );
+
+        return getReportPreviewSenderAvatar({
+            reportPreviewDetails,
+            personalDetails,
+            containerStyles: [styles.actionAvatar],
+            actorAccountID,
+        });
     };
 
-    const hasEmojiStatus = !displayAllActors && status?.emojiCode;
+    const hasEmojiStatus = !shouldDisplayAllActors && status?.emojiCode;
     const formattedDate = DateUtils.getStatusUntilDate(status?.clearAfter ?? '');
     const statusText = status?.text ?? '';
     const statusTooltipText = formattedDate ? `${statusText ? `${statusText} ` : ''}(${formattedDate})` : statusText;
@@ -331,11 +230,11 @@ function ReportActionItemSingle({
                                 <ReportActionItemFragment
                                     // eslint-disable-next-line react/no-array-index-key
                                     key={`person-${action?.reportActionID}-${index}`}
-                                    accountID={Number(delegatePersonalDetails && !isWorkspaceActor ? actorAccountID : (icon.id ?? CONST.DEFAULT_NUMBER_ID))}
+                                    accountID={Number(delegatePersonalDetails && !isWorkspaceActor ? actorAccountID : (primaryAvatar.id ?? CONST.DEFAULT_NUMBER_ID))}
                                     fragment={{...fragment, type: fragment.type ?? '', text: fragment.text ?? ''}}
                                     delegateAccountID={action?.delegateAccountID}
                                     isSingleLine
-                                    actorIcon={icon}
+                                    actorIcon={primaryAvatar}
                                     moderationDecision={getReportActionMessage(action)?.moderationDecision?.decision}
                                     shouldShowTooltip={!showMultipleUserAvatarPattern}
                                 />
diff --git a/src/pages/home/report/useReportPreviewDetails.tsx b/src/pages/home/report/useReportPreviewDetails.tsx
new file mode 100644
index 000000000000..f2e816bc3896
--- /dev/null
+++ b/src/pages/home/report/useReportPreviewDetails.tsx
@@ -0,0 +1,327 @@
+import React from 'react';
+import type {ViewStyle} from 'react-native';
+import {View} from 'react-native';
+import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
+import Avatar from '@components/Avatar';
+import {FallbackAvatar} from '@components/Icon/Expensicons';
+import UserDetailsTooltip from '@components/UserDetailsTooltip';
+import useOnyx from '@hooks/useOnyx';
+import {selectAllTransactionsForReport} from '@libs/MoneyRequestReportUtils';
+import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
+import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
+import {
+    getDefaultWorkspaceAvatar,
+    getDisplayNameForParticipant,
+    getIcons,
+    getPolicyName,
+    getReportActionActorAccountID,
+    getWorkspaceIcon,
+    isIndividualInvoiceRoom,
+    isInvoiceReport as isInvoiceReportUtils,
+    isInvoiceRoom,
+    isPolicyExpenseChat,
+    isTripRoom as isTripRoomReportUtils,
+} from '@libs/ReportUtils';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {PersonalDetailsList, Policy, Report, ReportAction, Transaction} from '@src/types/onyx';
+import type {Icon} from '@src/types/onyx/OnyxCommon';
+
+type AvatarDetails = {
+    reportPreviewSenderID: number | undefined;
+    reportPreviewAction: OnyxEntry<ReportAction>;
+    primaryAvatar: Icon;
+    secondaryAvatar: Icon;
+    shouldDisplayAllActors: boolean;
+    displayName: string;
+    isWorkspaceActor: boolean;
+    actorHint: string;
+    fallbackIcon: string | undefined;
+};
+
+type AvatarDetailsProps = {
+    personalDetails: OnyxEntry<PersonalDetailsList>;
+    innerPolicies: OnyxCollection<Policy>;
+    policy: OnyxEntry<Policy>;
+    action: OnyxEntry<ReportAction>;
+    report: OnyxEntry<Report>;
+    iouReport?: OnyxEntry<Report>;
+    policies?: OnyxCollection<Policy>;
+};
+
+function getSplitAuthor(transaction: Transaction, splits?: Array<ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU>>) {
+    const {originalTransactionID, source} = transaction.comment ?? {};
+
+    if (source !== CONST.IOU.TYPE.SPLIT || originalTransactionID === undefined) {
+        return undefined;
+    }
+
+    const splitAction = splits?.find((split) => getOriginalMessage(split)?.IOUTransactionID === originalTransactionID);
+
+    if (!splitAction) {
+        return undefined;
+    }
+
+    return splitAction.actorAccountID;
+}
+
+function getIconDetails({
+    action,
+    report,
+    iouReport,
+    policies,
+    personalDetails,
+    reportPreviewSenderID,
+    innerPolicies,
+    policy,
+}: AvatarDetailsProps & {reportPreviewSenderID: number | undefined}) {
+    const delegatePersonalDetails = action?.delegateAccountID ? personalDetails?.[action?.delegateAccountID] : undefined;
+    const actorAccountID = getReportActionActorAccountID(action, iouReport, report, delegatePersonalDetails);
+    const accountID = reportPreviewSenderID ?? actorAccountID ?? CONST.DEFAULT_NUMBER_ID;
+
+    const activePolicies = policies ?? innerPolicies;
+
+    const ownerAccountID = iouReport?.ownerAccountID ?? action?.childOwnerAccountID;
+    const isReportPreviewAction = action?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW;
+
+    const invoiceReceiverPolicy =
+        report?.invoiceReceiver && 'policyID' in report.invoiceReceiver ? activePolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report.invoiceReceiver.policyID}`] : undefined;
+
+    const {avatar, login, fallbackIcon} = personalDetails?.[accountID] ?? {};
+
+    const isTripRoom = isTripRoomReportUtils(report);
+    // We want to display only the sender's avatar next to the report preview if it only contains one person's expenses.
+    const displayAllActors = isReportPreviewAction && !isTripRoom && !isPolicyExpenseChat(report) && !reportPreviewSenderID;
+    const isInvoiceReport = isInvoiceReportUtils(iouReport ?? null);
+    const isWorkspaceActor = isInvoiceReport || (isPolicyExpenseChat(report) && (!actorAccountID || displayAllActors));
+
+    const getPrimaryAvatar = () => {
+        const defaultDisplayName = getDisplayNameForParticipant({accountID, personalDetailsData: personalDetails});
+        // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+        const actorHint = isWorkspaceActor ? getPolicyName({report, policy}) : (login || (defaultDisplayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '');
+
+        const defaultAvatar = {
+            source: avatar ?? FallbackAvatar,
+            id: actorAccountID,
+            name: defaultDisplayName,
+            type: CONST.ICON_TYPE_AVATAR,
+        };
+
+        if (isWorkspaceActor) {
+            return {
+                avatar: {
+                    ...defaultAvatar,
+                    name: getPolicyName({report, policy}),
+                    type: CONST.ICON_TYPE_WORKSPACE,
+                    source: getWorkspaceIcon(report, policy).source,
+                    id: report?.policyID,
+                },
+                actorHint,
+            };
+        }
+
+        if (delegatePersonalDetails) {
+            return {
+                avatar: {
+                    ...defaultAvatar,
+                    name: delegatePersonalDetails?.displayName ?? '',
+                    source: delegatePersonalDetails?.avatar ?? FallbackAvatar,
+                    id: delegatePersonalDetails?.accountID,
+                },
+                actorHint,
+            };
+        }
+
+        if (isReportPreviewAction && isTripRoom) {
+            return {
+                avatar: {
+                    ...defaultAvatar,
+                    name: report?.reportName ?? '',
+                    source: personalDetails?.[ownerAccountID ?? CONST.DEFAULT_NUMBER_ID]?.avatar ?? FallbackAvatar,
+                    id: ownerAccountID,
+                },
+                actorHint,
+            };
+        }
+
+        return {
+            avatar: defaultAvatar,
+            actorHint,
+        };
+    };
+
+    const getSecondaryAvatar = () => {
+        const defaultAvatar = {name: '', source: '', type: CONST.ICON_TYPE_AVATAR};
+
+        // If this is a report preview, display names and avatars of both people involved
+        if (displayAllActors) {
+            const secondaryAccountId = ownerAccountID === actorAccountID || isInvoiceReport ? actorAccountID : ownerAccountID;
+            const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar;
+            const secondaryDisplayName = getDisplayNameForParticipant({accountID: secondaryAccountId});
+            const secondaryPolicyAvatar = invoiceReceiverPolicy?.avatarURL ?? getDefaultWorkspaceAvatar(invoiceReceiverPolicy?.name);
+            const isWorkspaceInvoice = isInvoiceRoom(report) && !isIndividualInvoiceRoom(report);
+
+            return isWorkspaceInvoice
+                ? {
+                      source: secondaryPolicyAvatar,
+                      type: CONST.ICON_TYPE_WORKSPACE,
+                      name: invoiceReceiverPolicy?.name,
+                      id: invoiceReceiverPolicy?.id,
+                  }
+                : {
+                      source: secondaryUserAvatar,
+                      type: CONST.ICON_TYPE_AVATAR,
+                      name: secondaryDisplayName ?? '',
+                      id: secondaryAccountId,
+                  };
+        }
+
+        if (!isWorkspaceActor) {
+            // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+            const avatarIconIndex = report?.isOwnPolicyExpenseChat || isPolicyExpenseChat(report) ? 0 : 1;
+            const reportIcons = getIcons(report, personalDetails, undefined, undefined, undefined, policy);
+
+            return reportIcons.at(avatarIconIndex) ?? defaultAvatar;
+        }
+
+        if (isInvoiceReportUtils(iouReport)) {
+            const secondaryAccountId = iouReport?.managerID ?? CONST.DEFAULT_NUMBER_ID;
+            const secondaryUserAvatar = personalDetails?.[secondaryAccountId ?? -1]?.avatar ?? FallbackAvatar;
+            const secondaryDisplayName = getDisplayNameForParticipant({accountID: secondaryAccountId});
+
+            return {
+                source: secondaryUserAvatar,
+                type: CONST.ICON_TYPE_AVATAR,
+                name: secondaryDisplayName,
+                id: secondaryAccountId,
+            };
+        }
+
+        return defaultAvatar;
+    };
+
+    const {avatar: primaryAvatar, actorHint} = getPrimaryAvatar();
+
+    return {
+        primaryAvatar,
+        secondaryAvatar: getSecondaryAvatar(),
+        shouldDisplayAllActors: displayAllActors,
+        displayName: primaryAvatar.name,
+        isWorkspaceActor,
+        actorHint,
+        fallbackIcon,
+    };
+}
+
+/**
+ * This hook is used to determine the ID of the sender, as well as the avatars of the actors and some additional data, for the report preview action.
+ * It was originally based on actions; now, it uses transactions and unique emails as a fallback.
+ * For a reason why, see https://github.com/Expensify/App/pull/64802 discussion.
+ */
+function useReportPreviewDetails({iouReport, report, action, ...rest}: AvatarDetailsProps): AvatarDetails {
+    const [iouActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, {
+        canBeMissing: true,
+        selector: (actions) => Object.values(actions ?? {}).filter(isMoneyRequestAction),
+    });
+
+    const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
+        canBeMissing: true,
+        selector: (allTransactions) => selectAllTransactionsForReport(allTransactions, action?.childReportID, iouActions ?? []),
+    });
+
+    const [splits] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`, {
+        canBeMissing: true,
+        selector: (actions) =>
+            Object.values(actions ?? {})
+                .filter(isMoneyRequestAction)
+                .filter((act) => getOriginalMessage(act)?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT),
+    });
+
+    if (action?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) {
+        return {
+            reportPreviewSenderID: undefined,
+            reportPreviewAction: undefined,
+            ...getIconDetails({
+                ...rest,
+                action,
+                report,
+                iouReport,
+                reportPreviewSenderID: undefined,
+            }),
+        };
+    }
+
+    // 1. If all amounts have the same sign - either all amounts are positive or all amounts are negative.
+    // We have to do it this way because there can be a case when actions are not available
+    // See: https://github.com/Expensify/App/pull/64802#issuecomment-3008944401
+
+    const areAmountsSignsTheSame = new Set(transactions?.map((tr) => Math.sign(tr.amount))).size < 2;
+
+    // 2. If there is only one attendee - we check that by counting unique emails converted to account IDs in the attendees list.
+    // This is a fallback added because: https://github.com/Expensify/App/pull/64802#issuecomment-3007906310
+
+    const attendeesIDs = transactions
+        // If the transaction is a split, then attendees are not present as a property so we need to use a helper function.
+        ?.flatMap<number | undefined>((tr) =>
+            tr.comment?.attendees?.map((att) => (tr.comment?.source === CONST.IOU.TYPE.SPLIT ? getSplitAuthor(tr, splits) : getPersonalDetailByEmail(att.email)?.accountID)),
+        )
+        .filter((accountID) => !!accountID);
+
+    const isThereOnlyOneAttendee = new Set(attendeesIDs).size <= 1;
+
+    // If the action is a 'Send Money' flow, it will only have one transaction, but the person who sent the money is the child manager account, not the child owner account.
+    const isSendMoneyFlow = action?.childMoneyRequestCount === 0 && transactions?.length === 1;
+    const singleAvatarAccountID = isSendMoneyFlow ? action.childManagerAccountID : action?.childOwnerAccountID;
+
+    const reportPreviewSenderID = areAmountsSignsTheSame && isThereOnlyOneAttendee ? singleAvatarAccountID : undefined;
+
+    return {
+        reportPreviewSenderID,
+        reportPreviewAction: action,
+        ...getIconDetails({
+            ...rest,
+            action,
+            report,
+            iouReport,
+            reportPreviewSenderID,
+        }),
+    };
+}
+
+function getReportPreviewSenderAvatar({
+    reportPreviewDetails,
+    personalDetails,
+    containerStyles,
+    actorAccountID,
+}: {
+    reportPreviewDetails: AvatarDetails;
+    personalDetails: PersonalDetailsList | undefined;
+    containerStyles: ViewStyle[];
+    actorAccountID: number | null | undefined;
+}) {
+    const {primaryAvatar, isWorkspaceActor, fallbackIcon: reportFallbackIcon, reportPreviewAction} = reportPreviewDetails;
+    const delegatePersonalDetails = reportPreviewAction?.delegateAccountID ? personalDetails?.[reportPreviewAction?.delegateAccountID] : undefined;
+
+    return (
+        <UserDetailsTooltip
+            accountID={Number(delegatePersonalDetails && !isWorkspaceActor ? actorAccountID : (primaryAvatar.id ?? CONST.DEFAULT_NUMBER_ID))}
+            delegateAccountID={reportPreviewAction?.delegateAccountID}
+            icon={primaryAvatar}
+        >
+            <View>
+                <Avatar
+                    containerStyles={containerStyles}
+                    source={primaryAvatar.source}
+                    type={primaryAvatar.type}
+                    name={primaryAvatar.name}
+                    avatarID={primaryAvatar.id}
+                    fallbackIcon={reportFallbackIcon}
+                />
+            </View>
+        </UserDetailsTooltip>
+    );
+}
+
+export default useReportPreviewDetails;
+export {getReportPreviewSenderAvatar};
+export type {AvatarDetails};
diff --git a/src/pages/home/report/useReportPreviewSenderID.tsx b/src/pages/home/report/useReportPreviewSenderID.tsx
deleted file mode 100644
index 09778daa329e..000000000000
--- a/src/pages/home/report/useReportPreviewSenderID.tsx
+++ /dev/null
@@ -1,87 +0,0 @@
-import type {OnyxEntry} from 'react-native-onyx';
-import useOnyx from '@hooks/useOnyx';
-import {selectAllTransactionsForReport} from '@libs/MoneyRequestReportUtils';
-import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
-import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type {Report, ReportAction, Transaction} from '@src/types/onyx';
-
-function getSplitAuthor(transaction: Transaction, splits?: Array<ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU>>) {
-    const {originalTransactionID, source} = transaction.comment ?? {};
-
-    if (source !== CONST.IOU.TYPE.SPLIT || originalTransactionID === undefined) {
-        return undefined;
-    }
-
-    const splitAction = splits?.find((split) => getOriginalMessage(split)?.IOUTransactionID === originalTransactionID);
-
-    if (!splitAction) {
-        return undefined;
-    }
-
-    return splitAction.actorAccountID;
-}
-
-/**
- * This hook is used to determine the ID of the sender for the report preview action.
- * There are few fallbacks to determine the sender ID.
- *
- * It should not be used outside of this component.
- * The only reason it is exported to a hook is to make it easier to remove it in the future.
- *
- * For a reason why it is here, see https://github.com/Expensify/App/pull/64802 discussion
- */
-function useReportPreviewSenderID({action, iouReport, report}: {action: OnyxEntry<ReportAction>; iouReport: OnyxEntry<Report>; report: OnyxEntry<Report>}) {
-    const [iouActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, {
-        canBeMissing: true,
-        selector: (actions) => Object.values(actions ?? {}).filter(isMoneyRequestAction),
-    });
-
-    const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
-        canBeMissing: true,
-        selector: (allTransactions) => selectAllTransactionsForReport(allTransactions, action?.childReportID, iouActions ?? []),
-    });
-
-    const [splits] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`, {
-        canBeMissing: true,
-        selector: (actions) =>
-            Object.values(actions ?? {})
-                .filter(isMoneyRequestAction)
-                .filter((act) => getOriginalMessage(act)?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT),
-    });
-
-    if (action?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW) {
-        return;
-    }
-
-    /* We need to perform the following checks to determine if the report preview is a single avatar: */
-
-    // 1. If all amounts have the same sign - either all amounts are positive or all amounts are negative.
-    // We have to do this because there can be a case when actions are not available
-    // See: https://github.com/Expensify/App/pull/64802#issuecomment-3008944401
-
-    const areAmountsSignsTheSame = new Set(transactions?.map((tr) => Math.sign(tr.amount))).size < 2;
-
-    // 2. If there is only one attendee - we check that by counting unique emails converted to account IDs in the attendees list.
-    // This has to be done as there are cases when transaction amounts signs are the same until report is opened.
-    // See: https://github.com/Expensify/App/pull/64802#issuecomment-3007906310
-
-    const attendeesIDs = transactions
-        // If the transaction is a split, then attendees are not present so we need to use a helper function.
-        ?.flatMap<number | undefined>((tr) =>
-            tr.comment?.attendees?.map((att) => (tr.comment?.source === CONST.IOU.TYPE.SPLIT ? getSplitAuthor(tr, splits) : getPersonalDetailByEmail(att.email)?.accountID)),
-        )
-        // We filter out empty ID's
-        .filter((accountID) => !!accountID);
-
-    const isThereOnlyOneAttendee = new Set(attendeesIDs).size <= 1;
-
-    // If the action is a 'Send Money' flow, it will only have one transaction, but the person who sent the money is the child manager account, not the child owner account.
-    const isSendMoneyFlow = action?.childMoneyRequestCount === 0 && transactions?.length === 1;
-    const singleAvatarAccountID = isSendMoneyFlow ? action.childManagerAccountID : action?.childOwnerAccountID;
-
-    return areAmountsSignsTheSame && isThereOnlyOneAttendee ? singleAvatarAccountID : undefined;
-}
-
-export default useReportPreviewSenderID;
diff --git a/tests/unit/ReportActionItemSingleTest.ts b/tests/unit/ReportActionItemSingleTest.ts
index 0eb98e4b740f..3a8815cfd249 100644
--- a/tests/unit/ReportActionItemSingleTest.ts
+++ b/tests/unit/ReportActionItemSingleTest.ts
@@ -1,6 +1,6 @@
 import {renderHook, screen, waitFor} from '@testing-library/react-native';
 import Onyx from 'react-native-onyx';
-import useReportPreviewSenderID from '@pages/home/report/useReportPreviewSenderID';
+import useReportPreviewDetails from '@pages/home/report/useReportPreviewDetails';
 import CONST from '@src/CONST';
 import * as PersonalDetailsUtils from '@src/libs/PersonalDetailsUtils';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -8,6 +8,7 @@ import type {PersonalDetailsList} from '@src/types/onyx';
 import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet';
 import {actionR14932, actionR98765} from '../../__mocks__/reportData/actions';
 import personalDetails from '../../__mocks__/reportData/personalDetails';
+import {policy420A} from '../../__mocks__/reportData/policies';
 import {chatReportR14932, iouReportR14932} from '../../__mocks__/reportData/reports';
 import {transactionR14932} from '../../__mocks__/reportData/transactions';
 import * as LHNTestUtils from '../utils/LHNTestUtils';
@@ -103,7 +104,18 @@ const validAction = {
     childManagerAccountID: iouReportR14932.managerID,
 };
 
-describe('useReportPreviewSenderID', () => {
+describe('useAvatarDetails', () => {
+    const policiesMock = {
+        personalDetails,
+        policies: {
+            [`${ONYXKEYS.COLLECTION.POLICY}420A`]: policy420A,
+        },
+        innerPolicies: {
+            [`${ONYXKEYS.COLLECTION.POLICY}420A`]: policy420A,
+        },
+        policy: policy420A,
+    };
+
     const mockedEmailToID: Record<string, PropertyKeysOf<typeof personalDetails>> = {
         [personalDetails[15593135].login]: 15593135,
         [personalDetails[51760358].login]: 51760358,
@@ -131,24 +143,26 @@ describe('useReportPreviewSenderID', () => {
 
     it('returns undefined when action is not a report preview', () => {
         const {result} = renderHook(() =>
-            useReportPreviewSenderID({
+            useReportPreviewDetails({
                 action: actionR14932,
                 iouReport: iouReportR14932,
                 report: chatReportR14932,
+                ...policiesMock,
             }),
         );
-        expect(result.current).toBeUndefined();
+        expect(result.current.reportPreviewSenderID).toBeUndefined();
     });
 
     it('returns childManagerAccountID when all conditions are met for Send Money flow', () => {
         const {result} = renderHook(() =>
-            useReportPreviewSenderID({
+            useReportPreviewDetails({
                 action: {...validAction, childMoneyRequestCount: 0},
                 iouReport: iouReportR14932,
                 report: chatReportR14932,
+                ...policiesMock,
             }),
         );
-        expect(result.current).toBe(iouReportR14932.managerID);
+        expect(result.current.reportPreviewSenderID).toBe(iouReportR14932.managerID);
     });
 
     it('returns undefined when there are multiple attendees', async () => {
@@ -165,13 +179,14 @@ describe('useReportPreviewSenderID', () => {
             },
         });
         const {result} = renderHook(() =>
-            useReportPreviewSenderID({
+            useReportPreviewDetails({
                 action: validAction,
                 iouReport: iouReportR14932,
                 report: chatReportR14932,
+                ...policiesMock,
             }),
         );
-        expect(result.current).toBeUndefined();
+        expect(result.current.reportPreviewSenderID).toBeUndefined();
     });
 
     it('returns undefined when amounts have different signs', async () => {
@@ -184,23 +199,25 @@ describe('useReportPreviewSenderID', () => {
             amount: -100,
         });
         const {result} = renderHook(() =>
-            useReportPreviewSenderID({
+            useReportPreviewDetails({
                 action: validAction,
                 iouReport: iouReportR14932,
                 report: chatReportR14932,
+                ...policiesMock,
             }),
         );
-        expect(result.current).toBeUndefined();
+        expect(result.current.reportPreviewSenderID).toBeUndefined();
     });
 
     it('returns childOwnerAccountID when all conditions are met', () => {
         const {result} = renderHook(() =>
-            useReportPreviewSenderID({
+            useReportPreviewDetails({
                 action: validAction,
                 iouReport: iouReportR14932,
                 report: chatReportR14932,
+                ...policiesMock,
             }),
         );
-        expect(result.current).toBe(iouReportR14932.ownerAccountID);
+        expect(result.current.reportPreviewSenderID).toBe(iouReportR14932.ownerAccountID);
     });
 });

From 47b251a09174c7ab8e4046442117d46600bd8876 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Tue, 8 Jul 2025 11:27:33 +0200
Subject: [PATCH 15/18] Update useAvatarDetails test & small fixes

---
 src/components/AvatarWithDisplayName.tsx      |  4 +-
 src/components/HeaderWithBackButton/index.tsx |  1 +
 tests/unit/ReportActionItemSingleTest.ts      | 42 ++++++++++++++-----
 3 files changed, 35 insertions(+), 12 deletions(-)

diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx
index 1790af9d5d6a..cc57b530c185 100644
--- a/src/components/AvatarWithDisplayName.tsx
+++ b/src/components/AvatarWithDisplayName.tsx
@@ -245,7 +245,7 @@ function AvatarWithDisplayName({
 
     const shouldUseFullTitle = isMoneyRequestOrReport || isAnonymous;
 
-    const getAvatar = () => {
+    const getAvatar = useCallback(() => {
         if (shouldShowSubscriptAvatar) {
             return (
                 <SubscriptAvatar
@@ -273,7 +273,7 @@ function AvatarWithDisplayName({
             containerStyles: [styles.actionAvatar, styles.mr3],
             actorAccountID: actorAccountID?.current,
         });
-    };
+    }, [StyleUtils, avatarBorderColor, icons, personalDetails, shouldShowSubscriptAvatar, singleAvatarDetails, size, styles]);
 
     const avatar = <View accessibilityLabel={title}>{getAvatar()}</View>;
 
diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx
index 1d358f9ebddd..8d55acf70f70 100755
--- a/src/components/HeaderWithBackButton/index.tsx
+++ b/src/components/HeaderWithBackButton/index.tsx
@@ -140,6 +140,7 @@ function HeaderWithBackButton({
         titleColor,
         translate,
         openParentReportInCurrentTab,
+        singleAvatarDetails,
     ]);
     const ThreeDotMenuButton = useMemo(() => {
         if (shouldShowThreeDotsButton) {
diff --git a/tests/unit/ReportActionItemSingleTest.ts b/tests/unit/ReportActionItemSingleTest.ts
index 3a8815cfd249..b120ced0a85f 100644
--- a/tests/unit/ReportActionItemSingleTest.ts
+++ b/tests/unit/ReportActionItemSingleTest.ts
@@ -105,6 +105,13 @@ const validAction = {
 };
 
 describe('useAvatarDetails', () => {
+    const mockedOwnerAccountID = 15593135;
+    const mockedOwnerAccountAvatar = personalDetails[mockedOwnerAccountID].avatar;
+
+    const mockedManagerAccountID = 51760358;
+    const mockedManagerAccountAvatar = personalDetails[mockedManagerAccountID].avatar;
+    const mockedDMChatRoom = {...chatReportR14932, chatType: undefined};
+
     const policiesMock = {
         personalDetails,
         policies: {
@@ -141,31 +148,37 @@ describe('useAvatarDetails', () => {
         return waitForBatchedUpdates();
     });
 
-    it('returns undefined when action is not a report preview', () => {
+    it('returns avatar with no reportPreviewSenderID when action is not a report preview', () => {
         const {result} = renderHook(() =>
             useReportPreviewDetails({
                 action: actionR14932,
                 iouReport: iouReportR14932,
-                report: chatReportR14932,
+                report: mockedDMChatRoom,
                 ...policiesMock,
             }),
         );
+
+        expect(result.current.primaryAvatar.source).toBe(mockedOwnerAccountAvatar);
+        expect(result.current.secondaryAvatar.source).toBeFalsy();
         expect(result.current.reportPreviewSenderID).toBeUndefined();
     });
 
-    it('returns childManagerAccountID when all conditions are met for Send Money flow', () => {
+    it('returns childManagerAccountID and his avatar when all conditions are met for Send Money flow', () => {
         const {result} = renderHook(() =>
             useReportPreviewDetails({
                 action: {...validAction, childMoneyRequestCount: 0},
                 iouReport: iouReportR14932,
-                report: chatReportR14932,
+                report: mockedDMChatRoom,
                 ...policiesMock,
             }),
         );
+
+        expect(result.current.primaryAvatar.source).toBe(mockedManagerAccountAvatar);
+        expect(result.current.secondaryAvatar.source).toBeFalsy();
         expect(result.current.reportPreviewSenderID).toBe(iouReportR14932.managerID);
     });
 
-    it('returns undefined when there are multiple attendees', async () => {
+    it('returns both avatars & no reportPreviewSenderID when there are multiple attendees', async () => {
         await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}`, {
             ...transactionR14932,
             comment: {
@@ -182,14 +195,17 @@ describe('useAvatarDetails', () => {
             useReportPreviewDetails({
                 action: validAction,
                 iouReport: iouReportR14932,
-                report: chatReportR14932,
+                report: mockedDMChatRoom,
                 ...policiesMock,
             }),
         );
+
+        expect(result.current.primaryAvatar.source).toBe(mockedManagerAccountAvatar);
+        expect(result.current.secondaryAvatar.source).toBe(mockedOwnerAccountAvatar);
         expect(result.current.reportPreviewSenderID).toBeUndefined();
     });
 
-    it('returns undefined when amounts have different signs', async () => {
+    it('returns both avatars & no reportPreviewSenderID when amounts have different signs', async () => {
         await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}`, {
             ...transactionR14932,
             amount: 100,
@@ -202,22 +218,28 @@ describe('useAvatarDetails', () => {
             useReportPreviewDetails({
                 action: validAction,
                 iouReport: iouReportR14932,
-                report: chatReportR14932,
+                report: mockedDMChatRoom,
                 ...policiesMock,
             }),
         );
+
+        expect(result.current.primaryAvatar.source).toBe(mockedManagerAccountAvatar);
+        expect(result.current.secondaryAvatar.source).toBe(mockedOwnerAccountAvatar);
         expect(result.current.reportPreviewSenderID).toBeUndefined();
     });
 
-    it('returns childOwnerAccountID when all conditions are met', () => {
+    it('returns childOwnerAccountID as reportPreviewSenderID and a single avatar when all conditions are met', () => {
         const {result} = renderHook(() =>
             useReportPreviewDetails({
                 action: validAction,
                 iouReport: iouReportR14932,
-                report: chatReportR14932,
+                report: mockedDMChatRoom,
                 ...policiesMock,
             }),
         );
+
+        expect(result.current.primaryAvatar.source).toBe(mockedOwnerAccountAvatar);
+        expect(result.current.secondaryAvatar.source).toBeFalsy();
         expect(result.current.reportPreviewSenderID).toBe(iouReportR14932.ownerAccountID);
     });
 });

From 470d3895aa3a73c692586a084b424863f22cfb46 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Tue, 8 Jul 2025 12:00:05 +0200
Subject: [PATCH 16/18] Correct names & extract related methods & test

---
 src/components/AvatarWithDisplayName.tsx      |   8 +-
 src/components/HeaderWithBackButton/types.ts  |   4 +-
 src/components/MoneyReportHeader.tsx          |   4 +-
 .../useReportAvatarDetails.ts}                |  50 +-----
 src/libs/getReportSingleAvatar.tsx            |  44 +++++
 .../home/report/ReportActionItemSingle.tsx    |   7 +-
 tests/unit/ReportActionItemSingleTest.ts      | 165 +----------------
 tests/unit/useReportAvatarDetailsTest.ts      | 169 ++++++++++++++++++
 8 files changed, 231 insertions(+), 220 deletions(-)
 rename src/{pages/home/report/useReportPreviewDetails.tsx => hooks/useReportAvatarDetails.ts} (86%)
 create mode 100644 src/libs/getReportSingleAvatar.tsx
 create mode 100644 tests/unit/useReportAvatarDetailsTest.ts

diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx
index cc57b530c185..794c5e7c6a62 100644
--- a/src/components/AvatarWithDisplayName.tsx
+++ b/src/components/AvatarWithDisplayName.tsx
@@ -4,10 +4,12 @@ import type {ColorValue, TextStyle} from 'react-native';
 import type {OnyxEntry} from 'react-native-onyx';
 import type {ValueOf} from 'type-fest';
 import useOnyx from '@hooks/useOnyx';
+import type {ReportAvatarDetails} from '@hooks/useReportAvatarDetails';
 import useReportIsArchived from '@hooks/useReportIsArchived';
 import useStyleUtils from '@hooks/useStyleUtils';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
+import getReportSingleAvatar from '@libs/getReportSingleAvatar';
 import Navigation from '@libs/Navigation/Navigation';
 import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils';
 import type {DisplayNameWithTooltips} from '@libs/ReportUtils';
@@ -27,8 +29,6 @@ import {
     navigateToDetailsPage,
     shouldReportShowSubscript,
 } from '@libs/ReportUtils';
-import type {AvatarDetails} from '@pages/home/report/useReportPreviewDetails';
-import {getReportPreviewSenderAvatar} from '@pages/home/report/useReportPreviewDetails';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import ROUTES from '@src/ROUTES';
@@ -77,7 +77,7 @@ type AvatarWithDisplayNameProps = {
     avatarBorderColor?: ColorValue;
 
     /** If we want to override the default avatar behavior and set a single avatar, we should pass this prop. */
-    singleAvatarDetails?: AvatarDetails;
+    singleAvatarDetails?: ReportAvatarDetails;
 };
 
 const fallbackIcon: Icon = {
@@ -267,7 +267,7 @@ function AvatarWithDisplayName({
             );
         }
 
-        return getReportPreviewSenderAvatar({
+        return getReportSingleAvatar({
             reportPreviewDetails: singleAvatarDetails,
             personalDetails,
             containerStyles: [styles.actionAvatar, styles.mr3],
diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts
index f742884baf4f..db23738a9952 100644
--- a/src/components/HeaderWithBackButton/types.ts
+++ b/src/components/HeaderWithBackButton/types.ts
@@ -2,8 +2,8 @@ import type {ReactNode} from 'react';
 import type {StyleProp, ViewStyle} from 'react-native';
 import type {OnyxEntry} from 'react-native-onyx';
 import type {PopoverMenuItem} from '@components/PopoverMenu';
+import type {ReportAvatarDetails} from '@hooks/useReportAvatarDetails';
 import type {Action} from '@hooks/useSingleExecution';
-import type {AvatarDetails} from '@pages/home/report/useReportPreviewDetails';
 import type {StepCounterParams} from '@src/languages/params';
 import type {TranslationPaths} from '@src/languages/types';
 import type {AnchorPosition} from '@src/styles';
@@ -164,7 +164,7 @@ type HeaderWithBackButtonProps = Partial<ChildrenProps> & {
     openParentReportInCurrentTab?: boolean;
 
     /** If we want to override the default avatar behavior and set a single avatar, we should pass this prop. */
-    singleAvatarDetails?: AvatarDetails;
+    singleAvatarDetails?: ReportAvatarDetails;
 };
 
 export type {ThreeDotsMenuItem};
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 261dcf3143ff..4c652cb77b4c 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -10,6 +10,7 @@ import useNetwork from '@hooks/useNetwork';
 import useOnyx from '@hooks/useOnyx';
 import usePaymentAnimations from '@hooks/usePaymentAnimations';
 import usePaymentOptions from '@hooks/usePaymentOptions';
+import useReportAvatarDetails from '@hooks/useReportAvatarDetails';
 import useReportIsArchived from '@hooks/useReportIsArchived';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions';
@@ -60,7 +61,6 @@ import {
     shouldShowBrokenConnectionViolationForMultipleTransactions,
 } from '@libs/TransactionUtils';
 import type {ExportType} from '@pages/home/report/ReportDetailsExportPage';
-import useReportPreviewDetails from '@pages/home/report/useReportPreviewDetails';
 import variables from '@styles/variables';
 import {
     approveMoneyRequest,
@@ -230,7 +230,7 @@ function MoneyReportHeader({
         [allViolations, transactionIDs],
     );
 
-    const details = useReportPreviewDetails({report: chatReport, iouReport: moneyRequestReport, action: reportPreviewAction, policy, innerPolicies: policies, personalDetails});
+    const details = useReportAvatarDetails({report: chatReport, iouReport: moneyRequestReport, action: reportPreviewAction, policy, innerPolicies: policies, personalDetails});
 
     const messagePDF = useMemo(() => {
         if (!reportPDFFilename) {
diff --git a/src/pages/home/report/useReportPreviewDetails.tsx b/src/hooks/useReportAvatarDetails.ts
similarity index 86%
rename from src/pages/home/report/useReportPreviewDetails.tsx
rename to src/hooks/useReportAvatarDetails.ts
index f2e816bc3896..8815696094da 100644
--- a/src/pages/home/report/useReportPreviewDetails.tsx
+++ b/src/hooks/useReportAvatarDetails.ts
@@ -1,11 +1,5 @@
-import React from 'react';
-import type {ViewStyle} from 'react-native';
-import {View} from 'react-native';
 import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
-import Avatar from '@components/Avatar';
 import {FallbackAvatar} from '@components/Icon/Expensicons';
-import UserDetailsTooltip from '@components/UserDetailsTooltip';
-import useOnyx from '@hooks/useOnyx';
 import {selectAllTransactionsForReport} from '@libs/MoneyRequestReportUtils';
 import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
 import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
@@ -26,8 +20,9 @@ import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type {PersonalDetailsList, Policy, Report, ReportAction, Transaction} from '@src/types/onyx';
 import type {Icon} from '@src/types/onyx/OnyxCommon';
+import useOnyx from './useOnyx';
 
-type AvatarDetails = {
+type ReportAvatarDetails = {
     reportPreviewSenderID: number | undefined;
     reportPreviewAction: OnyxEntry<ReportAction>;
     primaryAvatar: Icon;
@@ -218,7 +213,7 @@ function getIconDetails({
  * It was originally based on actions; now, it uses transactions and unique emails as a fallback.
  * For a reason why, see https://github.com/Expensify/App/pull/64802 discussion.
  */
-function useReportPreviewDetails({iouReport, report, action, ...rest}: AvatarDetailsProps): AvatarDetails {
+function useReportAvatarDetails({iouReport, report, action, ...rest}: AvatarDetailsProps): ReportAvatarDetails {
     const [iouActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport?.reportID}`, {
         canBeMissing: true,
         selector: (actions) => Object.values(actions ?? {}).filter(isMoneyRequestAction),
@@ -288,40 +283,5 @@ function useReportPreviewDetails({iouReport, report, action, ...rest}: AvatarDet
     };
 }
 
-function getReportPreviewSenderAvatar({
-    reportPreviewDetails,
-    personalDetails,
-    containerStyles,
-    actorAccountID,
-}: {
-    reportPreviewDetails: AvatarDetails;
-    personalDetails: PersonalDetailsList | undefined;
-    containerStyles: ViewStyle[];
-    actorAccountID: number | null | undefined;
-}) {
-    const {primaryAvatar, isWorkspaceActor, fallbackIcon: reportFallbackIcon, reportPreviewAction} = reportPreviewDetails;
-    const delegatePersonalDetails = reportPreviewAction?.delegateAccountID ? personalDetails?.[reportPreviewAction?.delegateAccountID] : undefined;
-
-    return (
-        <UserDetailsTooltip
-            accountID={Number(delegatePersonalDetails && !isWorkspaceActor ? actorAccountID : (primaryAvatar.id ?? CONST.DEFAULT_NUMBER_ID))}
-            delegateAccountID={reportPreviewAction?.delegateAccountID}
-            icon={primaryAvatar}
-        >
-            <View>
-                <Avatar
-                    containerStyles={containerStyles}
-                    source={primaryAvatar.source}
-                    type={primaryAvatar.type}
-                    name={primaryAvatar.name}
-                    avatarID={primaryAvatar.id}
-                    fallbackIcon={reportFallbackIcon}
-                />
-            </View>
-        </UserDetailsTooltip>
-    );
-}
-
-export default useReportPreviewDetails;
-export {getReportPreviewSenderAvatar};
-export type {AvatarDetails};
+export default useReportAvatarDetails;
+export type {ReportAvatarDetails};
diff --git a/src/libs/getReportSingleAvatar.tsx b/src/libs/getReportSingleAvatar.tsx
new file mode 100644
index 000000000000..22b926ed0a0b
--- /dev/null
+++ b/src/libs/getReportSingleAvatar.tsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import type {ViewStyle} from 'react-native';
+import {View} from 'react-native';
+import Avatar from '@components/Avatar';
+import UserDetailsTooltip from '@components/UserDetailsTooltip';
+import type {ReportAvatarDetails} from '@hooks/useReportAvatarDetails';
+import CONST from '@src/CONST';
+import type {PersonalDetailsList} from '@src/types/onyx';
+
+function getReportSingleAvatar({
+    reportPreviewDetails,
+    personalDetails,
+    containerStyles,
+    actorAccountID,
+}: {
+    reportPreviewDetails: ReportAvatarDetails;
+    personalDetails: PersonalDetailsList | undefined;
+    containerStyles: ViewStyle[];
+    actorAccountID: number | null | undefined;
+}) {
+    const {primaryAvatar, isWorkspaceActor, fallbackIcon: reportFallbackIcon, reportPreviewAction} = reportPreviewDetails;
+    const delegatePersonalDetails = reportPreviewAction?.delegateAccountID ? personalDetails?.[reportPreviewAction?.delegateAccountID] : undefined;
+
+    return (
+        <UserDetailsTooltip
+            accountID={Number(delegatePersonalDetails && !isWorkspaceActor ? actorAccountID : (primaryAvatar.id ?? CONST.DEFAULT_NUMBER_ID))}
+            delegateAccountID={reportPreviewAction?.delegateAccountID}
+            icon={primaryAvatar}
+        >
+            <View>
+                <Avatar
+                    containerStyles={containerStyles}
+                    source={primaryAvatar.source}
+                    type={primaryAvatar.type}
+                    name={primaryAvatar.name}
+                    avatarID={primaryAvatar.id}
+                    fallbackIcon={reportFallbackIcon}
+                />
+            </View>
+        </UserDetailsTooltip>
+    );
+}
+
+export default getReportSingleAvatar;
diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index f1551770c46c..21196eb16ba1 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -11,11 +11,13 @@ import Tooltip from '@components/Tooltip';
 import useLocalize from '@hooks/useLocalize';
 import useOnyx from '@hooks/useOnyx';
 import usePolicy from '@hooks/usePolicy';
+import useReportAvatarDetails from '@hooks/useReportAvatarDetails';
 import useStyleUtils from '@hooks/useStyleUtils';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import ControlSelection from '@libs/ControlSelection';
 import DateUtils from '@libs/DateUtils';
+import getReportSingleAvatar from '@libs/getReportSingleAvatar';
 import Navigation from '@libs/Navigation/Navigation';
 import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
 import {getReportActionMessage} from '@libs/ReportActionsUtils';
@@ -27,7 +29,6 @@ import type {Policy, Report, ReportAction} from '@src/types/onyx';
 import type ChildrenProps from '@src/types/utils/ChildrenProps';
 import ReportActionItemDate from './ReportActionItemDate';
 import ReportActionItemFragment from './ReportActionItemFragment';
-import useReportPreviewDetails, {getReportPreviewSenderAvatar} from './useReportPreviewDetails';
 
 type ReportActionItemSingleProps = Partial<ChildrenProps> & {
     /** All the data of the action */
@@ -102,7 +103,7 @@ function ReportActionItemSingle({
     const delegatePersonalDetails = action?.delegateAccountID ? personalDetails?.[action?.delegateAccountID] : undefined;
     const actorAccountID = getReportActionActorAccountID(action, iouReport, report, delegatePersonalDetails);
 
-    const reportPreviewDetails = useReportPreviewDetails({
+    const reportPreviewDetails = useReportAvatarDetails({
         action,
         report,
         iouReport,
@@ -188,7 +189,7 @@ function ReportActionItemSingle({
             );
         }
 
-        return getReportPreviewSenderAvatar({
+        return getReportSingleAvatar({
             reportPreviewDetails,
             personalDetails,
             containerStyles: [styles.actionAvatar],
diff --git a/tests/unit/ReportActionItemSingleTest.ts b/tests/unit/ReportActionItemSingleTest.ts
index b120ced0a85f..9b32ec46634c 100644
--- a/tests/unit/ReportActionItemSingleTest.ts
+++ b/tests/unit/ReportActionItemSingleTest.ts
@@ -1,22 +1,13 @@
-import {renderHook, screen, waitFor} from '@testing-library/react-native';
+import {screen, waitFor} from '@testing-library/react-native';
 import Onyx from 'react-native-onyx';
-import useReportPreviewDetails from '@pages/home/report/useReportPreviewDetails';
 import CONST from '@src/CONST';
-import * as PersonalDetailsUtils from '@src/libs/PersonalDetailsUtils';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type {PersonalDetailsList} from '@src/types/onyx';
 import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet';
-import {actionR14932, actionR98765} from '../../__mocks__/reportData/actions';
-import personalDetails from '../../__mocks__/reportData/personalDetails';
-import {policy420A} from '../../__mocks__/reportData/policies';
-import {chatReportR14932, iouReportR14932} from '../../__mocks__/reportData/reports';
-import {transactionR14932} from '../../__mocks__/reportData/transactions';
 import * as LHNTestUtils from '../utils/LHNTestUtils';
 import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
 import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates';
 
-import PropertyKeysOf = jest.PropertyKeysOf;
-
 describe('ReportActionItemSingle', () => {
     beforeAll(() =>
         Onyx.init({
@@ -89,157 +80,3 @@ describe('ReportActionItemSingle', () => {
         });
     });
 });
-
-const reportActions = [{[actionR14932.reportActionID]: actionR14932}];
-const transactions = [transactionR14932];
-
-const transactionCollectionDataSet = toCollectionDataSet(ONYXKEYS.COLLECTION.TRANSACTION, transactions, (transaction) => transaction.transactionID);
-const reportActionCollectionDataSet = toCollectionDataSet(ONYXKEYS.COLLECTION.REPORT_ACTIONS, reportActions, (actions) => Object.values(actions).at(0)?.childReportID);
-
-const validAction = {
-    ...actionR98765,
-    childReportID: iouReportR14932.reportID,
-    actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW,
-    childOwnerAccountID: iouReportR14932.ownerAccountID,
-    childManagerAccountID: iouReportR14932.managerID,
-};
-
-describe('useAvatarDetails', () => {
-    const mockedOwnerAccountID = 15593135;
-    const mockedOwnerAccountAvatar = personalDetails[mockedOwnerAccountID].avatar;
-
-    const mockedManagerAccountID = 51760358;
-    const mockedManagerAccountAvatar = personalDetails[mockedManagerAccountID].avatar;
-    const mockedDMChatRoom = {...chatReportR14932, chatType: undefined};
-
-    const policiesMock = {
-        personalDetails,
-        policies: {
-            [`${ONYXKEYS.COLLECTION.POLICY}420A`]: policy420A,
-        },
-        innerPolicies: {
-            [`${ONYXKEYS.COLLECTION.POLICY}420A`]: policy420A,
-        },
-        policy: policy420A,
-    };
-
-    const mockedEmailToID: Record<string, PropertyKeysOf<typeof personalDetails>> = {
-        [personalDetails[15593135].login]: 15593135,
-        [personalDetails[51760358].login]: 51760358,
-    };
-
-    beforeAll(() => {
-        Onyx.init({
-            keys: ONYXKEYS,
-        });
-        jest.spyOn(PersonalDetailsUtils, 'getPersonalDetailByEmail').mockImplementation((email) => personalDetails[mockedEmailToID[email]]);
-    });
-
-    beforeEach(() => {
-        Onyx.multiSet({
-            ...reportActionCollectionDataSet,
-            ...transactionCollectionDataSet,
-        });
-        return waitForBatchedUpdates();
-    });
-
-    afterEach(() => {
-        Onyx.clear();
-        return waitForBatchedUpdates();
-    });
-
-    it('returns avatar with no reportPreviewSenderID when action is not a report preview', () => {
-        const {result} = renderHook(() =>
-            useReportPreviewDetails({
-                action: actionR14932,
-                iouReport: iouReportR14932,
-                report: mockedDMChatRoom,
-                ...policiesMock,
-            }),
-        );
-
-        expect(result.current.primaryAvatar.source).toBe(mockedOwnerAccountAvatar);
-        expect(result.current.secondaryAvatar.source).toBeFalsy();
-        expect(result.current.reportPreviewSenderID).toBeUndefined();
-    });
-
-    it('returns childManagerAccountID and his avatar when all conditions are met for Send Money flow', () => {
-        const {result} = renderHook(() =>
-            useReportPreviewDetails({
-                action: {...validAction, childMoneyRequestCount: 0},
-                iouReport: iouReportR14932,
-                report: mockedDMChatRoom,
-                ...policiesMock,
-            }),
-        );
-
-        expect(result.current.primaryAvatar.source).toBe(mockedManagerAccountAvatar);
-        expect(result.current.secondaryAvatar.source).toBeFalsy();
-        expect(result.current.reportPreviewSenderID).toBe(iouReportR14932.managerID);
-    });
-
-    it('returns both avatars & no reportPreviewSenderID when there are multiple attendees', async () => {
-        await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}`, {
-            ...transactionR14932,
-            comment: {
-                attendees: [{email: personalDetails[15593135].login, displayName: 'Test One', avatarUrl: 'https://none.com/none'}],
-            },
-        });
-        await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}2`, {
-            ...transactionR14932,
-            comment: {
-                attendees: [{email: personalDetails[51760358].login, displayName: 'Test Two', avatarUrl: 'https://none.com/none2'}],
-            },
-        });
-        const {result} = renderHook(() =>
-            useReportPreviewDetails({
-                action: validAction,
-                iouReport: iouReportR14932,
-                report: mockedDMChatRoom,
-                ...policiesMock,
-            }),
-        );
-
-        expect(result.current.primaryAvatar.source).toBe(mockedManagerAccountAvatar);
-        expect(result.current.secondaryAvatar.source).toBe(mockedOwnerAccountAvatar);
-        expect(result.current.reportPreviewSenderID).toBeUndefined();
-    });
-
-    it('returns both avatars & no reportPreviewSenderID when amounts have different signs', async () => {
-        await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}`, {
-            ...transactionR14932,
-            amount: 100,
-        });
-        await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}2`, {
-            ...transactionR14932,
-            amount: -100,
-        });
-        const {result} = renderHook(() =>
-            useReportPreviewDetails({
-                action: validAction,
-                iouReport: iouReportR14932,
-                report: mockedDMChatRoom,
-                ...policiesMock,
-            }),
-        );
-
-        expect(result.current.primaryAvatar.source).toBe(mockedManagerAccountAvatar);
-        expect(result.current.secondaryAvatar.source).toBe(mockedOwnerAccountAvatar);
-        expect(result.current.reportPreviewSenderID).toBeUndefined();
-    });
-
-    it('returns childOwnerAccountID as reportPreviewSenderID and a single avatar when all conditions are met', () => {
-        const {result} = renderHook(() =>
-            useReportPreviewDetails({
-                action: validAction,
-                iouReport: iouReportR14932,
-                report: mockedDMChatRoom,
-                ...policiesMock,
-            }),
-        );
-
-        expect(result.current.primaryAvatar.source).toBe(mockedOwnerAccountAvatar);
-        expect(result.current.secondaryAvatar.source).toBeFalsy();
-        expect(result.current.reportPreviewSenderID).toBe(iouReportR14932.ownerAccountID);
-    });
-});
diff --git a/tests/unit/useReportAvatarDetailsTest.ts b/tests/unit/useReportAvatarDetailsTest.ts
new file mode 100644
index 000000000000..4aa68fb0bfaf
--- /dev/null
+++ b/tests/unit/useReportAvatarDetailsTest.ts
@@ -0,0 +1,169 @@
+import {renderHook} from '@testing-library/react-native';
+import Onyx from 'react-native-onyx';
+import useReportAvatarDetails from '@hooks/useReportAvatarDetails';
+import CONST from '@src/CONST';
+import * as PersonalDetailsUtils from '@src/libs/PersonalDetailsUtils';
+import ONYXKEYS from '@src/ONYXKEYS';
+import {toCollectionDataSet} from '@src/types/utils/CollectionDataSet';
+import {actionR14932, actionR98765} from '../../__mocks__/reportData/actions';
+import personalDetails from '../../__mocks__/reportData/personalDetails';
+import {policy420A} from '../../__mocks__/reportData/policies';
+import {chatReportR14932, iouReportR14932} from '../../__mocks__/reportData/reports';
+import {transactionR14932} from '../../__mocks__/reportData/transactions';
+import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
+
+import PropertyKeysOf = jest.PropertyKeysOf;
+
+const reportActions = [{[actionR14932.reportActionID]: actionR14932}];
+const transactions = [transactionR14932];
+
+const transactionCollectionDataSet = toCollectionDataSet(ONYXKEYS.COLLECTION.TRANSACTION, transactions, (transaction) => transaction.transactionID);
+const reportActionCollectionDataSet = toCollectionDataSet(ONYXKEYS.COLLECTION.REPORT_ACTIONS, reportActions, (actions) => Object.values(actions).at(0)?.childReportID);
+
+const validAction = {
+    ...actionR98765,
+    childReportID: iouReportR14932.reportID,
+    actionName: CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW,
+    childOwnerAccountID: iouReportR14932.ownerAccountID,
+    childManagerAccountID: iouReportR14932.managerID,
+};
+
+describe('useReportAvatarDetails', () => {
+    const mockedOwnerAccountID = 15593135;
+    const mockedOwnerAccountAvatar = personalDetails[mockedOwnerAccountID].avatar;
+
+    const mockedManagerAccountID = 51760358;
+    const mockedManagerAccountAvatar = personalDetails[mockedManagerAccountID].avatar;
+    const mockedDMChatRoom = {...chatReportR14932, chatType: undefined};
+
+    const policiesMock = {
+        personalDetails,
+        policies: {
+            [`${ONYXKEYS.COLLECTION.POLICY}420A`]: policy420A,
+        },
+        innerPolicies: {
+            [`${ONYXKEYS.COLLECTION.POLICY}420A`]: policy420A,
+        },
+        policy: policy420A,
+    };
+
+    const mockedEmailToID: Record<string, PropertyKeysOf<typeof personalDetails>> = {
+        [personalDetails[15593135].login]: 15593135,
+        [personalDetails[51760358].login]: 51760358,
+    };
+
+    beforeAll(() => {
+        Onyx.init({
+            keys: ONYXKEYS,
+        });
+        jest.spyOn(PersonalDetailsUtils, 'getPersonalDetailByEmail').mockImplementation((email) => personalDetails[mockedEmailToID[email]]);
+    });
+
+    beforeEach(() => {
+        Onyx.multiSet({
+            ...reportActionCollectionDataSet,
+            ...transactionCollectionDataSet,
+        });
+        return waitForBatchedUpdates();
+    });
+
+    afterEach(() => {
+        Onyx.clear();
+        return waitForBatchedUpdates();
+    });
+
+    it('returns avatar with no reportPreviewSenderID when action is not a report preview', () => {
+        const {result} = renderHook(() =>
+            useReportAvatarDetails({
+                action: actionR14932,
+                iouReport: iouReportR14932,
+                report: mockedDMChatRoom,
+                ...policiesMock,
+            }),
+        );
+
+        expect(result.current.primaryAvatar.source).toBe(mockedOwnerAccountAvatar);
+        expect(result.current.secondaryAvatar.source).toBeFalsy();
+        expect(result.current.reportPreviewSenderID).toBeUndefined();
+    });
+
+    it('returns childManagerAccountID and his avatar when all conditions are met for Send Money flow', () => {
+        const {result} = renderHook(() =>
+            useReportAvatarDetails({
+                action: {...validAction, childMoneyRequestCount: 0},
+                iouReport: iouReportR14932,
+                report: mockedDMChatRoom,
+                ...policiesMock,
+            }),
+        );
+
+        expect(result.current.primaryAvatar.source).toBe(mockedManagerAccountAvatar);
+        expect(result.current.secondaryAvatar.source).toBeFalsy();
+        expect(result.current.reportPreviewSenderID).toBe(iouReportR14932.managerID);
+    });
+
+    it('returns both avatars & no reportPreviewSenderID when there are multiple attendees', async () => {
+        await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}`, {
+            ...transactionR14932,
+            comment: {
+                attendees: [{email: personalDetails[15593135].login, displayName: 'Test One', avatarUrl: 'https://none.com/none'}],
+            },
+        });
+        await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}2`, {
+            ...transactionR14932,
+            comment: {
+                attendees: [{email: personalDetails[51760358].login, displayName: 'Test Two', avatarUrl: 'https://none.com/none2'}],
+            },
+        });
+        const {result} = renderHook(() =>
+            useReportAvatarDetails({
+                action: validAction,
+                iouReport: iouReportR14932,
+                report: mockedDMChatRoom,
+                ...policiesMock,
+            }),
+        );
+
+        expect(result.current.primaryAvatar.source).toBe(mockedManagerAccountAvatar);
+        expect(result.current.secondaryAvatar.source).toBe(mockedOwnerAccountAvatar);
+        expect(result.current.reportPreviewSenderID).toBeUndefined();
+    });
+
+    it('returns both avatars & no reportPreviewSenderID when amounts have different signs', async () => {
+        await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}`, {
+            ...transactionR14932,
+            amount: 100,
+        });
+        await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionR14932.transactionID}2`, {
+            ...transactionR14932,
+            amount: -100,
+        });
+        const {result} = renderHook(() =>
+            useReportAvatarDetails({
+                action: validAction,
+                iouReport: iouReportR14932,
+                report: mockedDMChatRoom,
+                ...policiesMock,
+            }),
+        );
+
+        expect(result.current.primaryAvatar.source).toBe(mockedManagerAccountAvatar);
+        expect(result.current.secondaryAvatar.source).toBe(mockedOwnerAccountAvatar);
+        expect(result.current.reportPreviewSenderID).toBeUndefined();
+    });
+
+    it('returns childOwnerAccountID as reportPreviewSenderID and a single avatar when all conditions are met', () => {
+        const {result} = renderHook(() =>
+            useReportAvatarDetails({
+                action: validAction,
+                iouReport: iouReportR14932,
+                report: mockedDMChatRoom,
+                ...policiesMock,
+            }),
+        );
+
+        expect(result.current.primaryAvatar.source).toBe(mockedOwnerAccountAvatar);
+        expect(result.current.secondaryAvatar.source).toBeFalsy();
+        expect(result.current.reportPreviewSenderID).toBe(iouReportR14932.ownerAccountID);
+    });
+});

From 0c438fcf5f0f8007e0c3c8a8b5558df0976f21a5 Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Tue, 8 Jul 2025 12:10:58 +0200
Subject: [PATCH 17/18] Change getSingleReportAvatar to the component

---
 src/components/AvatarWithDisplayName.tsx         | 16 +++++++++-------
 .../ReportActionItem/SingleReportAvatar.tsx}     |  4 ++--
 src/pages/home/report/ReportActionItemSingle.tsx | 16 +++++++++-------
 3 files changed, 20 insertions(+), 16 deletions(-)
 rename src/{libs/getReportSingleAvatar.tsx => components/ReportActionItem/SingleReportAvatar.tsx} (95%)

diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx
index 794c5e7c6a62..b28e57ce6020 100644
--- a/src/components/AvatarWithDisplayName.tsx
+++ b/src/components/AvatarWithDisplayName.tsx
@@ -3,13 +3,13 @@ import {View} from 'react-native';
 import type {ColorValue, TextStyle} from 'react-native';
 import type {OnyxEntry} from 'react-native-onyx';
 import type {ValueOf} from 'type-fest';
+import SingleReportAvatar from '@components/ReportActionItem/SingleReportAvatar';
 import useOnyx from '@hooks/useOnyx';
 import type {ReportAvatarDetails} from '@hooks/useReportAvatarDetails';
 import useReportIsArchived from '@hooks/useReportIsArchived';
 import useStyleUtils from '@hooks/useStyleUtils';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
-import getReportSingleAvatar from '@libs/getReportSingleAvatar';
 import Navigation from '@libs/Navigation/Navigation';
 import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils';
 import type {DisplayNameWithTooltips} from '@libs/ReportUtils';
@@ -267,12 +267,14 @@ function AvatarWithDisplayName({
             );
         }
 
-        return getReportSingleAvatar({
-            reportPreviewDetails: singleAvatarDetails,
-            personalDetails,
-            containerStyles: [styles.actionAvatar, styles.mr3],
-            actorAccountID: actorAccountID?.current,
-        });
+        return (
+            <SingleReportAvatar
+                reportPreviewDetails={singleAvatarDetails}
+                personalDetails={personalDetails}
+                containerStyles={[styles.actionAvatar, styles.mr3]}
+                actorAccountID={actorAccountID.current}
+            />
+        );
     }, [StyleUtils, avatarBorderColor, icons, personalDetails, shouldShowSubscriptAvatar, singleAvatarDetails, size, styles]);
 
     const avatar = <View accessibilityLabel={title}>{getAvatar()}</View>;
diff --git a/src/libs/getReportSingleAvatar.tsx b/src/components/ReportActionItem/SingleReportAvatar.tsx
similarity index 95%
rename from src/libs/getReportSingleAvatar.tsx
rename to src/components/ReportActionItem/SingleReportAvatar.tsx
index 22b926ed0a0b..9a7c25d6808b 100644
--- a/src/libs/getReportSingleAvatar.tsx
+++ b/src/components/ReportActionItem/SingleReportAvatar.tsx
@@ -7,7 +7,7 @@ import type {ReportAvatarDetails} from '@hooks/useReportAvatarDetails';
 import CONST from '@src/CONST';
 import type {PersonalDetailsList} from '@src/types/onyx';
 
-function getReportSingleAvatar({
+function SingleReportAvatar({
     reportPreviewDetails,
     personalDetails,
     containerStyles,
@@ -41,4 +41,4 @@ function getReportSingleAvatar({
     );
 }
 
-export default getReportSingleAvatar;
+export default SingleReportAvatar;
diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx
index 21196eb16ba1..0a79a2e77e70 100644
--- a/src/pages/home/report/ReportActionItemSingle.tsx
+++ b/src/pages/home/report/ReportActionItemSingle.tsx
@@ -5,6 +5,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
 import MultipleAvatars from '@components/MultipleAvatars';
 import OfflineWithFeedback from '@components/OfflineWithFeedback';
 import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
+import SingleReportAvatar from '@components/ReportActionItem/SingleReportAvatar';
 import SubscriptAvatar from '@components/SubscriptAvatar';
 import Text from '@components/Text';
 import Tooltip from '@components/Tooltip';
@@ -17,7 +18,6 @@ import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import ControlSelection from '@libs/ControlSelection';
 import DateUtils from '@libs/DateUtils';
-import getReportSingleAvatar from '@libs/getReportSingleAvatar';
 import Navigation from '@libs/Navigation/Navigation';
 import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils';
 import {getReportActionMessage} from '@libs/ReportActionsUtils';
@@ -189,12 +189,14 @@ function ReportActionItemSingle({
             );
         }
 
-        return getReportSingleAvatar({
-            reportPreviewDetails,
-            personalDetails,
-            containerStyles: [styles.actionAvatar],
-            actorAccountID,
-        });
+        return (
+            <SingleReportAvatar
+                reportPreviewDetails={reportPreviewDetails}
+                personalDetails={personalDetails}
+                containerStyles={[styles.actionAvatar]}
+                actorAccountID={actorAccountID}
+            />
+        );
     };
 
     const hasEmojiStatus = !shouldDisplayAllActors && status?.emojiCode;

From 84b21baf468553b79164f9943854791a1b63f99f Mon Sep 17 00:00:00 2001
From: Jakub Korytko <jakub@korytko.me>
Date: Tue, 8 Jul 2025 12:31:03 +0200
Subject: [PATCH 18/18] Fix ESLint check

---
 src/components/AvatarWithDisplayName.tsx | 92 ++++++++++++++----------
 1 file changed, 54 insertions(+), 38 deletions(-)

diff --git a/src/components/AvatarWithDisplayName.tsx b/src/components/AvatarWithDisplayName.tsx
index b28e57ce6020..688d4b9bdaf5 100644
--- a/src/components/AvatarWithDisplayName.tsx
+++ b/src/components/AvatarWithDisplayName.tsx
@@ -3,7 +3,6 @@ import {View} from 'react-native';
 import type {ColorValue, TextStyle} from 'react-native';
 import type {OnyxEntry} from 'react-native-onyx';
 import type {ValueOf} from 'type-fest';
-import SingleReportAvatar from '@components/ReportActionItem/SingleReportAvatar';
 import useOnyx from '@hooks/useOnyx';
 import type {ReportAvatarDetails} from '@hooks/useReportAvatarDetails';
 import useReportIsArchived from '@hooks/useReportIsArchived';
@@ -41,6 +40,7 @@ import {FallbackAvatar} from './Icon/Expensicons';
 import MultipleAvatars from './MultipleAvatars';
 import ParentNavigationSubtitle from './ParentNavigationSubtitle';
 import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
+import SingleReportAvatar from './ReportActionItem/SingleReportAvatar';
 import type {TransactionListItemType} from './SelectionList/types';
 import SubscriptAvatar from './SubscriptAvatar';
 import Text from './Text';
@@ -245,55 +245,71 @@ function AvatarWithDisplayName({
 
     const shouldUseFullTitle = isMoneyRequestOrReport || isAnonymous;
 
-    const getAvatar = useCallback(() => {
-        if (shouldShowSubscriptAvatar) {
+    const getAvatar = useCallback(
+        (accountID: number) => {
+            if (shouldShowSubscriptAvatar) {
+                return (
+                    <SubscriptAvatar
+                        backgroundColor={avatarBorderColor}
+                        mainAvatar={icons.at(0) ?? fallbackIcon}
+                        secondaryAvatar={icons.at(1)}
+                        size={size}
+                    />
+                );
+            }
+
+            if (!singleAvatarDetails || singleAvatarDetails.shouldDisplayAllActors || !singleAvatarDetails.reportPreviewSenderID) {
+                return (
+                    <MultipleAvatars
+                        icons={icons}
+                        size={size}
+                        secondAvatarStyle={[StyleUtils.getBackgroundAndBorderStyle(avatarBorderColor)]}
+                    />
+                );
+            }
+
             return (
-                <SubscriptAvatar
-                    backgroundColor={avatarBorderColor}
-                    mainAvatar={icons.at(0) ?? fallbackIcon}
-                    secondaryAvatar={icons.at(1)}
-                    size={size}
+                <SingleReportAvatar
+                    reportPreviewDetails={singleAvatarDetails}
+                    personalDetails={personalDetails}
+                    containerStyles={[styles.actionAvatar, styles.mr3]}
+                    actorAccountID={accountID}
                 />
             );
-        }
+        },
+        [StyleUtils, avatarBorderColor, icons, personalDetails, shouldShowSubscriptAvatar, singleAvatarDetails, size, styles],
+    );
+
+    const getWrappedAvatar = useCallback(
+        (accountID: number) => {
+            const avatar = getAvatar(accountID);
+
+            if (!shouldEnableAvatarNavigation) {
+                return <View accessibilityLabel={title}>{avatar}</View>;
+            }
 
-        if (!singleAvatarDetails || singleAvatarDetails.shouldDisplayAllActors || !singleAvatarDetails.reportPreviewSenderID) {
             return (
-                <MultipleAvatars
-                    icons={icons}
-                    size={size}
-                    secondAvatarStyle={[StyleUtils.getBackgroundAndBorderStyle(avatarBorderColor)]}
-                />
+                <View accessibilityLabel={title}>
+                    <PressableWithoutFeedback
+                        onPress={showActorDetails}
+                        accessibilityLabel={title}
+                        role={getButtonRole(true)}
+                    >
+                        {avatar}
+                    </PressableWithoutFeedback>
+                </View>
             );
-        }
-
-        return (
-            <SingleReportAvatar
-                reportPreviewDetails={singleAvatarDetails}
-                personalDetails={personalDetails}
-                containerStyles={[styles.actionAvatar, styles.mr3]}
-                actorAccountID={actorAccountID.current}
-            />
-        );
-    }, [StyleUtils, avatarBorderColor, icons, personalDetails, shouldShowSubscriptAvatar, singleAvatarDetails, size, styles]);
+        },
+        [getAvatar, shouldEnableAvatarNavigation, showActorDetails, title],
+    );
 
-    const avatar = <View accessibilityLabel={title}>{getAvatar()}</View>;
+    const WrappedAvatar = getWrappedAvatar(actorAccountID?.current ?? CONST.DEFAULT_NUMBER_ID);
 
     const headerView = (
         <View style={[styles.appContentHeaderTitle, styles.flex1]}>
             {!!report && !!title && (
                 <View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, styles.justifyContentBetween]}>
-                    {shouldEnableAvatarNavigation ? (
-                        <PressableWithoutFeedback
-                            onPress={showActorDetails}
-                            accessibilityLabel={title}
-                            role={getButtonRole(true)}
-                        >
-                            {avatar}
-                        </PressableWithoutFeedback>
-                    ) : (
-                        avatar
-                    )}
+                    {WrappedAvatar}
                     <View style={[styles.flex1, styles.flexColumn]}>
                         {getCustomDisplayName(
                             shouldUseCustomSearchTitleName,
