Skip to content

Commit 8b25111

Browse files
authored
Merge pull request Expensify#87507 from callstack-internal/perf-send-message-phase-3
refactor: PureReportActionItem, add ActionableWhisperContent
2 parents 0c68091 + c19843b commit 8b25111

9 files changed

Lines changed: 546 additions & 181 deletions

src/libs/actions/Report/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5319,7 +5319,7 @@ function resolveActionableMentionWhisper(
53195319

53205320
function resolveActionableMentionConfirmWhisper(
53215321
report: OnyxEntry<Report>,
5322-
reportAction: OnyxEntry<ReportAction>,
5322+
reportAction: ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER>,
53235323
resolution: ValueOf<typeof CONST.REPORT.ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER>,
53245324
isReportArchived: boolean,
53255325
) {

src/pages/inbox/report/PureReportActionItem.tsx

Lines changed: 56 additions & 180 deletions
Large diffs are not rendered by default.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import {View} from 'react-native';
3+
import type {OnyxEntry} from 'react-native-onyx';
4+
import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext';
5+
import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons';
6+
import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons';
7+
import useReportIsArchived from '@hooks/useReportIsArchived';
8+
import {resolveActionableMentionConfirmWhisper} from '@libs/actions/Report';
9+
import ReportActionItemMessage from '@pages/inbox/report/ReportActionItemMessage';
10+
import CONST from '@src/CONST';
11+
import type {Report, ReportAction} from '@src/types/onyx';
12+
13+
type ConfirmWhisperContentProps = {
14+
action: ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER>;
15+
reportID: string | undefined;
16+
originalReportID: string | undefined;
17+
report: OnyxEntry<Report>;
18+
originalReport: OnyxEntry<Report>;
19+
};
20+
21+
function ConfirmWhisperContent({action, reportID, originalReportID, report, originalReport}: ConfirmWhisperContentProps) {
22+
const reportActionReport = originalReport ?? report;
23+
const isOriginalReportArchived = useReportIsArchived(originalReportID);
24+
const mentionReportContextValue = {currentReportID: report?.reportID, exactlyMatch: true};
25+
26+
const buttons: ActionableItem[] = [
27+
{
28+
text: 'common.buttonConfirm',
29+
key: `${action.reportActionID}-actionableReportMentionConfirmWhisper-${CONST.REPORT.ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER.DONE}`,
30+
onPress: () =>
31+
resolveActionableMentionConfirmWhisper(reportActionReport, action, CONST.REPORT.ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER.DONE, isOriginalReportArchived),
32+
isPrimary: true,
33+
},
34+
];
35+
36+
return (
37+
<MentionReportContext.Provider value={mentionReportContextValue}>
38+
<View>
39+
<ReportActionItemMessage
40+
action={action}
41+
reportID={reportID}
42+
displayAsGroup
43+
/>
44+
<ActionableItemButtons
45+
items={buttons}
46+
shouldUseLocalization
47+
layout="horizontal"
48+
/>
49+
</View>
50+
</MentionReportContext.Provider>
51+
);
52+
}
53+
54+
ConfirmWhisperContent.displayName = 'ConfirmWhisperContent';
55+
56+
export default ConfirmWhisperContent;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react';
2+
import {View} from 'react-native';
3+
import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons';
4+
import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons';
5+
import useLocalize from '@hooks/useLocalize';
6+
import {getActionableCardFraudAlertMessage, getOriginalMessage} from '@libs/ReportActionsUtils';
7+
import ReportActionItemBasicMessage from '@pages/inbox/report/ReportActionItemBasicMessage';
8+
import {resolveFraudAlert} from '@userActions/Card';
9+
import CONST from '@src/CONST';
10+
import type {ReportAction} from '@src/types/onyx';
11+
12+
type FraudAlertContentProps = {
13+
action: ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT>;
14+
reportID: string | undefined;
15+
};
16+
17+
function FraudAlertContent({action, reportID}: FraudAlertContentProps) {
18+
const {translate, getLocalDateFromDatetime} = useLocalize();
19+
20+
const reportActionID = action?.reportActionID;
21+
const originalMessage = getOriginalMessage(action);
22+
const cardID = originalMessage?.cardID;
23+
24+
const buttons: ActionableItem[] = originalMessage?.resolution
25+
? []
26+
: [
27+
{
28+
text: 'cardPage.cardFraudAlert.confirmButtonText',
29+
key: `${action.reportActionID}-cardFraudAlert-confirm`,
30+
onPress: () => {
31+
resolveFraudAlert(cardID, false, reportID, reportActionID);
32+
},
33+
isPrimary: true,
34+
},
35+
{
36+
text: 'cardPage.cardFraudAlert.reportFraudButtonText',
37+
key: `${action.reportActionID}-cardFraudAlert-reportFraud`,
38+
onPress: () => {
39+
resolveFraudAlert(cardID, true, reportID, reportActionID);
40+
},
41+
},
42+
];
43+
const message = getActionableCardFraudAlertMessage(translate, action, getLocalDateFromDatetime);
44+
45+
return (
46+
<View
47+
accessibilityRole={CONST.ROLE.ALERT}
48+
accessibilityLabel={translate('reportFraudConfirmationPage.title')}
49+
>
50+
<ReportActionItemBasicMessage message={message} />
51+
{buttons.length > 0 && (
52+
<ActionableItemButtons
53+
items={buttons}
54+
shouldUseLocalization
55+
layout="horizontal"
56+
/>
57+
)}
58+
</View>
59+
);
60+
}
61+
62+
FraudAlertContent.displayName = 'FraudAlertContent';
63+
64+
export default FraudAlertContent;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import React from 'react';
2+
import {View} from 'react-native';
3+
import type {OnyxEntry} from 'react-native-onyx';
4+
import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons';
5+
import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons';
6+
import useLocalize from '@hooks/useLocalize';
7+
import {getJoinRequestMessage, getOriginalMessage} from '@libs/ReportActionsUtils';
8+
import ReportActionItemBasicMessage from '@pages/inbox/report/ReportActionItemBasicMessage';
9+
import {acceptJoinRequest, declineJoinRequest} from '@userActions/Policy/Member';
10+
import CONST from '@src/CONST';
11+
import type {Policy, ReportAction} from '@src/types/onyx';
12+
import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage';
13+
14+
type JoinRequestContentProps = {
15+
action: ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_JOIN_REQUEST>;
16+
reportID: string | undefined;
17+
originalReportID: string;
18+
policy: OnyxEntry<Policy>;
19+
};
20+
21+
function JoinRequestContent({action, reportID, originalReportID, policy}: JoinRequestContentProps) {
22+
const {translate} = useLocalize();
23+
24+
const reportActionReportID = originalReportID ?? reportID;
25+
const buttons: ActionableItem[] =
26+
getOriginalMessage(action)?.choice !== ('' as JoinWorkspaceResolution)
27+
? []
28+
: [
29+
{
30+
text: 'actionableMentionJoinWorkspaceOptions.accept',
31+
key: `${action.reportActionID}-actionableMentionJoinWorkspace-${CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.ACCEPT}`,
32+
onPress: () => acceptJoinRequest(reportActionReportID, action),
33+
isPrimary: true,
34+
},
35+
{
36+
text: 'actionableMentionJoinWorkspaceOptions.decline',
37+
key: `${action.reportActionID}-actionableMentionJoinWorkspace-${CONST.REPORT.ACTIONABLE_MENTION_JOIN_WORKSPACE_RESOLUTION.DECLINE}`,
38+
onPress: () => declineJoinRequest(reportActionReportID, action),
39+
},
40+
];
41+
42+
return (
43+
<View>
44+
<ReportActionItemBasicMessage message={getJoinRequestMessage(translate, policy, action)} />
45+
{buttons.length > 0 && (
46+
<ActionableItemButtons
47+
items={buttons}
48+
shouldUseLocalization
49+
layout="horizontal"
50+
/>
51+
)}
52+
</View>
53+
);
54+
}
55+
56+
JoinRequestContent.displayName = 'JoinRequestContent';
57+
58+
export default JoinRequestContent;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import React from 'react';
2+
import type {OnyxEntry} from 'react-native-onyx';
3+
import type {ValueOf} from 'type-fest';
4+
import RenderHTML from '@components/RenderHTML';
5+
import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons';
6+
import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons';
7+
import useLocalize from '@hooks/useLocalize';
8+
import useReportIsArchived from '@hooks/useReportIsArchived';
9+
import {isPolicyAdmin, isPolicyMember, isPolicyOwner} from '@libs/PolicyUtils';
10+
import {getActionableMentionWhisperMessage, getOriginalMessage, isSystemUserMentioned} from '@libs/ReportActionsUtils';
11+
import ReportActionItemBasicMessage from '@pages/inbox/report/ReportActionItemBasicMessage';
12+
import CONST from '@src/CONST';
13+
import type {Policy, Report, ReportAction} from '@src/types/onyx';
14+
15+
type MentionWhisperContentProps = {
16+
action: ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_MENTION_WHISPER>;
17+
report: OnyxEntry<Report>;
18+
originalReport: OnyxEntry<Report>;
19+
policy: OnyxEntry<Policy>;
20+
currentUserAccountID: number;
21+
personalPolicyID: string | undefined;
22+
originalReportID: string | undefined;
23+
resolveActionableMentionWhisper: (
24+
report: OnyxEntry<Report>,
25+
reportAction: OnyxEntry<ReportAction>,
26+
resolution: ValueOf<typeof CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION>,
27+
isReportArchived: boolean,
28+
) => void;
29+
};
30+
31+
function MentionWhisperContent({
32+
action,
33+
report,
34+
originalReport,
35+
policy,
36+
currentUserAccountID,
37+
personalPolicyID,
38+
originalReportID,
39+
resolveActionableMentionWhisper,
40+
}: MentionWhisperContentProps) {
41+
const {translate} = useLocalize();
42+
const isOriginalReportArchived = useReportIsArchived(originalReportID);
43+
44+
const reportActionReport = originalReport ?? report;
45+
const reportPolicyID = report?.policyID;
46+
47+
const isReportInPolicy = !!reportPolicyID && reportPolicyID !== CONST.POLICY.ID_FAKE && personalPolicyID !== reportPolicyID;
48+
const hasMentionedPolicyMembers = getOriginalMessage(action)?.inviteeEmails?.every((login) => isPolicyMember(policy, login));
49+
50+
const buttons: ActionableItem[] = [];
51+
if ((isPolicyAdmin(policy) || isPolicyOwner(policy, currentUserAccountID)) && isReportInPolicy && !isSystemUserMentioned(action) && !hasMentionedPolicyMembers) {
52+
buttons.push({
53+
text: 'actionableMentionWhisperOptions.inviteToSubmitExpense',
54+
key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE}`,
55+
onPress: () => resolveActionableMentionWhisper(reportActionReport, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE_TO_SUBMIT_EXPENSE, isOriginalReportArchived),
56+
});
57+
}
58+
buttons.push(
59+
{
60+
text: 'actionableMentionWhisperOptions.inviteToChat',
61+
key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE}`,
62+
onPress: () => resolveActionableMentionWhisper(reportActionReport, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.INVITE, isOriginalReportArchived),
63+
},
64+
{
65+
text: 'actionableMentionWhisperOptions.nothing',
66+
key: `${action.reportActionID}-actionableMentionWhisper-${CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING}`,
67+
onPress: () => resolveActionableMentionWhisper(reportActionReport, action, CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION.NOTHING, isOriginalReportArchived),
68+
},
69+
);
70+
71+
return (
72+
<ReportActionItemBasicMessage>
73+
<RenderHTML html={getActionableMentionWhisperMessage(translate, action)} />
74+
{buttons.length > 0 && (
75+
<ActionableItemButtons
76+
items={buttons}
77+
shouldUseLocalization
78+
layout="vertical"
79+
/>
80+
)}
81+
</ReportActionItemBasicMessage>
82+
);
83+
}
84+
85+
MentionWhisperContent.displayName = 'MentionWhisperContent';
86+
87+
export default MentionWhisperContent;
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import React from 'react';
2+
import {View} from 'react-native';
3+
import type {OnyxEntry} from 'react-native-onyx';
4+
import type {ValueOf} from 'type-fest';
5+
import MentionReportContext from '@components/HTMLEngineProvider/HTMLRenderers/MentionReportRenderer/MentionReportContext';
6+
import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons';
7+
import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons';
8+
import {getOriginalMessage} from '@libs/ReportActionsUtils';
9+
import ReportActionItemMessage from '@pages/inbox/report/ReportActionItemMessage';
10+
import CONST from '@src/CONST';
11+
import type {Report, ReportAction} from '@src/types/onyx';
12+
13+
type ReportMentionWhisperContentProps = {
14+
action: ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_REPORT_MENTION_WHISPER>;
15+
reportID: string | undefined;
16+
report: OnyxEntry<Report>;
17+
originalReport: OnyxEntry<Report>;
18+
isReportArchived: boolean;
19+
resolveActionableReportMentionWhisper: (
20+
report: OnyxEntry<Report>,
21+
reportAction: OnyxEntry<ReportAction>,
22+
resolution: ValueOf<typeof CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION>,
23+
isReportArchived?: boolean,
24+
) => void;
25+
};
26+
27+
function ReportMentionWhisperContent({action, reportID, report, originalReport, isReportArchived, resolveActionableReportMentionWhisper}: ReportMentionWhisperContentProps) {
28+
const reportActionReport = originalReport ?? report;
29+
const resolution = getOriginalMessage(action)?.resolution;
30+
const mentionReportContextValue = {currentReportID: report?.reportID, exactlyMatch: true};
31+
32+
const buttons: ActionableItem[] = resolution
33+
? []
34+
: [
35+
{
36+
text: 'common.yes',
37+
key: `${action.reportActionID}-actionableReportMentionWhisper-${CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.CREATE}`,
38+
onPress: () => resolveActionableReportMentionWhisper(reportActionReport, action, CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.CREATE, isReportArchived),
39+
isPrimary: true,
40+
},
41+
{
42+
text: 'common.no',
43+
key: `${action.reportActionID}-actionableReportMentionWhisper-${CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.NOTHING}`,
44+
onPress: () => resolveActionableReportMentionWhisper(reportActionReport, action, CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION.NOTHING, isReportArchived),
45+
},
46+
];
47+
48+
return (
49+
<MentionReportContext.Provider value={mentionReportContextValue}>
50+
<View>
51+
<ReportActionItemMessage
52+
action={action}
53+
reportID={reportID}
54+
displayAsGroup
55+
/>
56+
{buttons.length > 0 && (
57+
<ActionableItemButtons
58+
items={buttons}
59+
shouldUseLocalization
60+
layout="horizontal"
61+
/>
62+
)}
63+
</View>
64+
</MentionReportContext.Provider>
65+
);
66+
}
67+
68+
ReportMentionWhisperContent.displayName = 'ReportMentionWhisperContent';
69+
70+
export default ReportMentionWhisperContent;

tests/ui/PureReportActionItemTest.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2214,6 +2214,29 @@ describe('PureReportActionItem', () => {
22142214
expect(screen.getByText(/requested to join/i)).toBeOnTheScreen();
22152215
});
22162216

2217+
it('isActionableReportMentionWhisper renders message and yes/no buttons', async () => {
2218+
const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_REPORT_MENTION_WHISPER, {});
2219+
const messageText = "Heads up, #test5 doesn't exist yet. Do you want to create it?";
2220+
action.message = [{type: 'COMMENT', html: messageText, text: messageText}];
2221+
renderItemWithAction(action);
2222+
await waitForBatchedUpdatesWithAct();
2223+
2224+
expect(screen.getByText(/Heads up, #test5 doesn't exist yet/)).toBeOnTheScreen();
2225+
expect(screen.getByText(translateLocal('common.yes'))).toBeOnTheScreen();
2226+
expect(screen.getByText(translateLocal('common.no'))).toBeOnTheScreen();
2227+
});
2228+
2229+
it('isActionableMentionInviteToSubmitExpenseConfirmWhisper renders message and confirm button', async () => {
2230+
const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER, {});
2231+
const messageText = "Great, you chose to invite them to the workspace! I've invited user and they'll submit expenses in their expense chat.";
2232+
action.message = [{type: 'COMMENT', html: messageText, text: messageText}];
2233+
renderItemWithAction(action);
2234+
await waitForBatchedUpdatesWithAct();
2235+
2236+
expect(screen.getByText(/Great, you chose to invite them/)).toBeOnTheScreen();
2237+
expect(screen.getByText(translateLocal('common.buttonConfirm'))).toBeOnTheScreen();
2238+
});
2239+
22172240
it('isCardIssuedAction renders card issued message', async () => {
22182241
const action = createReportAction(CONST.REPORT.ACTIONS.TYPE.CARD_ISSUED, {});
22192242
renderItemWithAction(action);

0 commit comments

Comments
 (0)